diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 34c8238648..cab36647d2 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -620,6 +620,18 @@ public @interface net.corda.core.contracts.LegalProseReference public abstract String uri() ## @CordaSerializable +public final class net.corda.core.contracts.LinearPointer extends net.corda.core.contracts.StatePointer + public (net.corda.core.contracts.UniqueIdentifier, Class) + @NotNull + public net.corda.core.contracts.UniqueIdentifier getPointer() + @NotNull + public Class getType() + @NotNull + public net.corda.core.contracts.StateAndRef resolve(net.corda.core.node.ServiceHub) + @NotNull + public net.corda.core.contracts.StateAndRef resolve(net.corda.core.transactions.LedgerTransaction) +## +@CordaSerializable public interface net.corda.core.contracts.LinearState extends net.corda.core.contracts.ContractState @NotNull public abstract net.corda.core.contracts.UniqueIdentifier getLinearId() @@ -763,6 +775,17 @@ public final class net.corda.core.contracts.StateAndRef extends java.lang.Object public String toString() ## @CordaSerializable +public abstract class net.corda.core.contracts.StatePointer extends java.lang.Object + @NotNull + public abstract Object getPointer() + @NotNull + public abstract Class getType() + @NotNull + public abstract net.corda.core.contracts.StateAndRef resolve(net.corda.core.node.ServiceHub) + @NotNull + public abstract net.corda.core.contracts.StateAndRef resolve(net.corda.core.transactions.LedgerTransaction) +## +@CordaSerializable public final class net.corda.core.contracts.StateRef extends java.lang.Object public (net.corda.core.crypto.SecureHash, int) @NotNull @@ -778,6 +801,18 @@ public final class net.corda.core.contracts.StateRef extends java.lang.Object @NotNull public String toString() ## +@CordaSerializable +public final class net.corda.core.contracts.StaticPointer extends net.corda.core.contracts.StatePointer + public (net.corda.core.contracts.StateRef, Class) + @NotNull + public net.corda.core.contracts.StateRef getPointer() + @NotNull + public Class getType() + @NotNull + public net.corda.core.contracts.StateAndRef resolve(net.corda.core.node.ServiceHub) + @NotNull + public net.corda.core.contracts.StateAndRef resolve(net.corda.core.transactions.LedgerTransaction) +## public final class net.corda.core.contracts.Structures extends java.lang.Object @NotNull public static final net.corda.core.crypto.SecureHash hash(net.corda.core.contracts.ContractState) diff --git a/core/src/main/kotlin/net/corda/core/contracts/StatePointer.kt b/core/src/main/kotlin/net/corda/core/contracts/StatePointer.kt new file mode 100644 index 0000000000..5f1cc39401 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/contracts/StatePointer.kt @@ -0,0 +1,121 @@ +package net.corda.core.contracts + +import net.corda.core.node.ServiceHub +import net.corda.core.node.services.Vault +import net.corda.core.node.services.queryBy +import net.corda.core.node.services.vault.QueryCriteria +import net.corda.core.serialization.CordaSerializable +import net.corda.core.transactions.LedgerTransaction + +/** + * A [StatePointer] contains a [pointer] to a [ContractState]. The [StatePointer] can be included in a [ContractState] + * or included in an off-ledger data structure. [StatePointer]s can be resolved to a [StateAndRef] by performing a + * vault query. There are two types of pointers; linear and static. [LinearPointer]s are for use with [LinearState]s. + * [StaticPointer]s are for use with any type of [ContractState]. + */ +@CordaSerializable +sealed class StatePointer { + /** + * An identifier for the [ContractState] that this [StatePointer] points to. + */ + abstract val pointer: Any + + /** + * Type of the state which is being pointed to. + */ + abstract val type: Class + + /** + * Resolves a [StatePointer] to a [StateAndRef] via a vault query. This method will either return a [StateAndRef] + * or return an exception. + * + * @param services a [ServiceHub] implementation is required to resolve the pointer. + */ + abstract fun resolve(services: ServiceHub): StateAndRef + + /** + * Resolves a [StatePointer] to a [StateAndRef] from inside a [LedgerTransaction]. The intuition here is that all + * of the pointed-to states will be included in the transaction as reference states. + * + * @param ltx the [LedgerTransaction] containing the [pointer] and pointed-to states. + */ + abstract fun resolve(ltx: LedgerTransaction): StateAndRef +} + +/** + * A [StaticPointer] contains a [pointer] to a specific [StateRef] and can be resolved by looking up the [StateRef] via + * [ServiceHub]. There are a number of things to keep in mind when using [StaticPointer]s: + * - The [ContractState] being pointed to may be spent or unspent when the [pointer] is resolved + * - The [ContractState] may not be known by the node performing the look-up in which case the [resolve] method will + * throw a [TransactionResolutionException] + */ +class StaticPointer(override val pointer: StateRef, override val type: Class) : StatePointer() { + /** + * Resolves a [StaticPointer] to a [StateAndRef] via a [StateRef] look-up. + */ + @Throws(TransactionResolutionException::class) + @Suppress("UNCHECKED_CAST") + override fun resolve(services: ServiceHub): StateAndRef { + val transactionState = services.loadState(pointer) as TransactionState + val castState: T = type.cast(transactionState.data) + val castTransactionState: TransactionState = transactionState.copy(data = castState) + return StateAndRef(castTransactionState, pointer) + } + + override fun resolve(ltx: LedgerTransaction): StateAndRef { + return ltx.referenceInputRefsOfType(type).single { pointer == it.ref } + } +} + +/** + * [LinearPointer] allows a [ContractState] to "point" to another [LinearState] creating a "many-to-one" relationship + * between all the states containing the pointer to a particular [LinearState] and the [LinearState] being pointed to. + * Using the [LinearPointer] is useful when one state depends on the data contained within another state that evolves + * independently. When using [LinearPointer] it is worth noting: + * - The node performing the resolution may not have seen any [LinearState]s with the specified [linearId], as such the + * vault query in [resolve] will return null and [resolve] will throw an exception + * - The node performing the resolution may not have the latest version of the [LinearState] and therefore will return + * an older version of the [LinearState]. As the pointed-to state will be added as a reference state to the transaction + * then the transaction with such a reference state cannot be committed to the ledger until the most up-to-date version + * of the [LinearState] is available. See reference states documentation on docs.corda.net for more info. + */ +class LinearPointer(override val pointer: UniqueIdentifier, override val type: Class) : StatePointer() { + /** + * Resolves a [LinearPointer] using the [UniqueIdentifier] contained in the [pointer] property. Returns a + * [StateAndRef] containing the latest version of the [LinearState] that the node calling [resolve] is aware of. + * + * @param services a [ServiceHub] implementation is required to perform a vault query. + */ + @Suppress("UNCHECKED_CAST") + override fun resolve(services: ServiceHub): StateAndRef { + // Return the latest version of the linear state. + // This query will only ever return one or zero states. + val query = QueryCriteria.LinearStateQueryCriteria( + linearId = listOf(pointer), + status = Vault.StateStatus.UNCONSUMED, + relevancyStatus = Vault.RelevancyStatus.ALL + ) + val result: List> = services.vaultService.queryBy(query).states + + check(result.isNotEmpty()) { + // Here either one of two things has happened: + // 1. The pointed-to state has not been seen by the resolver node. It is unlikely that this is the case. + // The state can probably be obtained via subscribing to the data distribution group which created and + // and maintains this data. + // 2. Uh oh... The pointed-to state has been exited from the ledger! + // It is unlikely this would ever happen as most reference data states will be created such that they cannot + // be exited from the ledger. At this point there are two options; use an old consumed version of the state, + // or don't use it at all. + "The LinearState with ID ${pointer.id} is unknown to this node or it has been exited from the ledger." + } + + val stateAndRef = result.single() + val castState: T = type.cast(stateAndRef.state.data) + val castTransactionState = stateAndRef.state.copy(data = castState) as TransactionState + return StateAndRef(castTransactionState, stateAndRef.ref) + } + + override fun resolve(ltx: LedgerTransaction): StateAndRef { + return ltx.referenceInputRefsOfType(type).single { pointer == it.state.data.linearId } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/internal/StatePointerSearch.kt b/core/src/main/kotlin/net/corda/core/internal/StatePointerSearch.kt new file mode 100644 index 0000000000..60e8b93482 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/internal/StatePointerSearch.kt @@ -0,0 +1,80 @@ +package net.corda.core.internal + +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.LinearPointer +import net.corda.core.contracts.StatePointer +import net.corda.core.contracts.StaticPointer +import java.lang.reflect.Field +import java.util.* + +/** + * Uses reflection to search for instances of [StatePointer] within a [ContractState]. + */ +class StatePointerSearch(val state: ContractState) { + // Classes in these packages should not be part of a search. + private val blackListedPackages = setOf("java.", "javax.") + + // Type required for traversal. + private data class FieldWithObject(val obj: Any, val field: Field) + + // List containing all discovered state pointers. + private val statePointers = mutableSetOf>() + + // Record seen objects to avoid getting stuck in loops. + private val seenObjects = mutableSetOf().apply { add(state) } + + // Queue of fields to search. + private val fieldQueue = ArrayDeque().apply { addAllFields(state) } + + // Helper for adding all fields to the queue. + private fun ArrayDeque.addAllFields(obj: Any) { + val fields = obj::class.java.declaredFields + val fieldsWithObjects = fields.mapNotNull { field -> + // Ignore classes which have not been loaded. + // Assumption: all required state classes are already loaded. + val packageName = field.type.`package`?.name + if (packageName == null) { + null + } else { + // Ignore JDK classes. + val isBlacklistedPackage = blackListedPackages.any { packageName.startsWith(it) } + if (isBlacklistedPackage) { + null + } else { + FieldWithObject(obj, field) + } + } + } + addAll(fieldsWithObjects) + } + + private fun handleField(obj: Any, field: Field) { + when { + // StatePointer. Handles nullable StatePointers too. + field.type == LinearPointer::class.java -> statePointers.add(field.get(obj) as? LinearPointer<*> ?: return) + field.type == StaticPointer::class.java -> statePointers.add(field.get(obj) as? StaticPointer<*> ?: return) + // Not StatePointer. + else -> { + val newObj = field.get(obj) ?: return + + // Ignore nulls. + if (newObj in seenObjects) { + return + } + + // Recurse. + fieldQueue.addAllFields(newObj) + seenObjects.add(obj) + } + } + } + + fun search(): Set> { + while (fieldQueue.isNotEmpty()) { + val (obj, field) = fieldQueue.pop() + field.isAccessible = true + handleField(obj, field) + } + return statePointers + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt b/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt index f707a8e308..aec3891862 100644 --- a/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt +++ b/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt @@ -2,6 +2,7 @@ package net.corda.core.node import net.corda.core.CordaRuntimeException import net.corda.core.KeepForDJVM +import net.corda.core.crypto.toStringShort import net.corda.core.identity.Party import net.corda.core.node.services.AttachmentId import net.corda.core.serialization.CordaSerializable @@ -140,7 +141,7 @@ data class NetworkParameters( modifiedTime=$modifiedTime epoch=$epoch, packageOwnership= { - ${packageOwnership.keys.joinToString()}} + ${packageOwnership.entries.joinToString("\n ") { "$it.key -> ${it.value.toStringShort()}" }} } }""" } @@ -172,7 +173,7 @@ class ZoneVersionTooLowException(message: String) : CordaRuntimeException(messag @CordaSerializable data class JavaPackageName(val name: String) { init { - require(isPackageValid(name)) { "Attempting to whitelist illegal java package: $name" } + require(isPackageValid(name)) { "Invalid Java package name: $name" } } /** @@ -182,7 +183,9 @@ data class JavaPackageName(val name: String) { * Note: The ownership check is ignoring case to prevent people from just releasing a jar with: "com.megaCorp.megatoken" and pretend they are MegaCorp. * By making the check case insensitive, the node will require that the jar is signed by MegaCorp, so the attack fails. */ - fun owns(fullClassName: String) = fullClassName.startsWith("${name}.", ignoreCase = true) + fun owns(fullClassName: String) = fullClassName.startsWith("$name.", ignoreCase = true) + + override fun toString() = name } // Check if a string is a legal Java package name. diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt index 74a787859a..a449440c09 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -12,6 +12,7 @@ import net.corda.core.crypto.keys import net.corda.core.identity.Party import net.corda.core.internal.AttachmentWithContext import net.corda.core.internal.FlowStateMachine +import net.corda.core.internal.StatePointerSearch import net.corda.core.internal.ensureMinimumPlatformVersion import net.corda.core.internal.isUploaderTrusted import net.corda.core.node.NetworkParameters @@ -83,7 +84,8 @@ open class TransactionBuilder @JvmOverloads constructor( protected val commands: MutableList> = arrayListOf(), protected var window: TimeWindow? = null, protected var privacySalt: PrivacySalt = PrivacySalt(), - protected val references: MutableList = arrayListOf() + protected val references: MutableList = arrayListOf(), + protected val serviceHub: ServiceHub? = (Strand.currentStrand() as? FlowStateMachine<*>)?.serviceHub ) { private companion object { @@ -105,7 +107,8 @@ open class TransactionBuilder @JvmOverloads constructor( commands = ArrayList(commands), window = window, privacySalt = privacySalt, - references = references + references = references, + serviceHub = serviceHub ) t.inputsWithTransactionState.addAll(this.inputsWithTransactionState) t.referencesWithTransactionState.addAll(this.referencesWithTransactionState) @@ -436,6 +439,44 @@ open class TransactionBuilder @JvmOverloads constructor( private fun checkReferencesUseSameNotary() = referencesWithTransactionState.map { it.notary }.toSet().size == 1 + /** + * If any inputs or outputs added to the [TransactionBuilder] contain [StatePointer]s, then this method can be + * optionally called to resolve those [StatePointer]s to [StateAndRef]s. The [StateAndRef]s are then added as + * reference states to the transaction. The effect is that the referenced data is carried along with the + * transaction. This may or may not be appropriate in all circumstances, which is why calling this method is + * optional. + * + * If this method is called outside the context of a flow, a [ServiceHub] instance must be passed to this method + * for it to be able to resolve [StatePointer]s. Usually for a unit test, this will be an instance of mock services. + * + * @param serviceHub a [ServiceHub] instance needed for performing vault queries. + * + * @throws IllegalStateException if no [ServiceHub] is provided and no flow context is available. + */ + private fun resolveStatePointers(transactionState: TransactionState<*>) { + val contractState = transactionState.data + // Find pointers in all inputs and outputs. + val inputAndOutputPointers = StatePointerSearch(contractState).search() + // Queue up the pointers to resolve. + val statePointerQueue = ArrayDeque>().apply { addAll(inputAndOutputPointers) } + // Recursively resolve all pointers. + while (statePointerQueue.isNotEmpty()) { + val nextStatePointer = statePointerQueue.pop() + if (serviceHub != null) { + val resolvedStateAndRef = nextStatePointer.resolve(serviceHub) + // Don't add dupe reference states because CoreTransaction doesn't allow it. + if (resolvedStateAndRef.ref !in referenceStates()) { + addReferenceState(resolvedStateAndRef.referenced()) + } + } else { + logger.warn("WARNING: You must pass in a ServiceHub reference to TransactionBuilder to resolve " + + "state pointers outside of flows. If you are writing a unit test then pass in a " + + "MockServices instance.") + return + } + } + } + /** * Adds a reference input [StateRef] to the transaction. * @@ -462,6 +503,10 @@ open class TransactionBuilder @JvmOverloads constructor( "Transactions with reference states using multiple different notaries are currently unsupported." } + // State Pointers are recursively resolved. NOTE: That this might be expensive. + // TODO: Add support for making recursive resolution optional if it becomes an issue. + resolveStatePointers(stateAndRef.state) + checkNotary(stateAndRef) references.add(stateAndRef.ref) checkForInputsAndReferencesOverlap() @@ -472,6 +517,8 @@ open class TransactionBuilder @JvmOverloads constructor( checkNotary(stateAndRef) inputs.add(stateAndRef.ref) inputsWithTransactionState.add(stateAndRef.state) + resolveStatePointers(stateAndRef.state) + return this } /** Adds an attachment with the specified hash to the TransactionBuilder. */ @@ -482,6 +529,8 @@ open class TransactionBuilder @JvmOverloads constructor( /** Adds an output state to the transaction. */ fun addOutputState(state: TransactionState<*>) = apply { outputs.add(state) + resolveStatePointers(state) + return this } /** Adds an output state, with associated contract code (and constraints), and notary, to the transaction. */ diff --git a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt index 82702514b6..8ef7904a75 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt @@ -29,6 +29,10 @@ infix fun Int.exactAdd(b: Int): Int = Math.addExact(this, b) /** Like the + operator but throws [ArithmeticException] in case of integer overflow. */ infix fun Long.exactAdd(b: Long): Long = Math.addExact(this, b) +/** There is no special case function for filtering null values out of a map in the stdlib */ +@Suppress("UNCHECKED_CAST") +fun Map.filterNotNullValues() = filterValues { it != null } as Map + /** * Usually you won't need this method: * * If you're in a companion object, use [contextLogger] diff --git a/docs/requirements.txt b/docs/requirements.txt index bf8d5e0d98..78f8604e3b 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -18,7 +18,7 @@ Pygments==2.2.0 pyparsing==2.2.0 pytz==2016.4 reportlab==3.4.0 -requests==2.18.4 +requests==2.20.0 rst2pdf==0.93 six==1.10.0 snowballstemmer==1.2.1 diff --git a/docs/source/api-states.rst b/docs/source/api-states.rst index 156a38fcd7..6b9e6ed0ef 100644 --- a/docs/source/api-states.rst +++ b/docs/source/api-states.rst @@ -225,3 +225,32 @@ then the check below will fail. transaction that references the encumbered state. This is because the data contained within the encumbered state may take on a different meaning, and likely would do, once the encumbrance state is taken into account. + +State Pointers +-------------- + +A ``StatePointer`` contains a pointer to a ``ContractState``. The ``StatePointer`` can be included in a ``ContractState`` as a +property, or included in an off-ledger data structure. ``StatePointer`` s can be resolved to a ``StateAndRef`` by performing +a look-up. There are two types of pointers; linear and static. + +1. ``StaticPointer`` s are for use with any type of ``ContractState``. The ``StaticPointer`` does as it suggests, it always + points to the same ``ContractState``. +2. The ``LinearPointer`` is for use with ``LinearState`` s. They are particularly useful because due to the way ``LinearState`` s + work, the pointer will automatically point you to the latest version of a ``LinearState`` that the node performing ``resolve`` + is aware of. In effect, the pointer "moves" as the ``LinearState`` is updated. + + ``StatePointer`` s do not enable a feature in Corda which was unavailable before. Rather, they help to formalise a pattern + which was already possible. In that light it is worth nothing some issues which you may encounter with `StatePointer` s: + +* If the node calling ``resolve`` has not seen any transactions containing a ``ContractState`` which the ``StatePointer`` + points to, then ``resolve`` will throw an exception. Here, the node calling ``resolve`` might be missing some crucial data. +* The node calling ``resolve`` for a ``LinearPointer`` may have seen and stored transactions containing a ``LinearState`` with + the specified ``linearId``. However, there is no guarantee the ``StateAndRef`` returned by ``resolve`` is the most recent + version of the ``LinearState``. The node only returns the most recent version that _it_ is aware of. + +**Resolving state pointers in `TransactionBuilder`** + +When building transactions, any ``StatePointer`` s contained within inputs or outputs added to a ``TransactionBuilder`` can +be optionally resolved to reference states using the ``resolveStatePointers`` method. The effect is that the pointed to +data is carried along with the transaction. This may or may not be appropriate in all circumstances, which is why +calling the method is optional. \ No newline at end of file diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index a066ba335d..ee92de0c22 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -7,6 +7,9 @@ release, see :doc:`upgrade-notes`. Unreleased ---------- +* Introduced new optional network bootstrapper command line options (--register-package-owner, --unregister-package-owner) + to register/unregister a java package namespace with an associated owner in the network parameter packageOwnership whitelist. + * New "validate-configuration" sub-command to `corda.jar`, allowing to validate the actual node configuration without starting the node. * Introduced new optional network bootstrapper command line option (--minimum-platform-version) to set as a network parameter diff --git a/docs/source/cipher-suites.rst b/docs/source/cipher-suites.rst index 13a4bf9ae7..8f2d313c45 100644 --- a/docs/source/cipher-suites.rst +++ b/docs/source/cipher-suites.rst @@ -1,5 +1,5 @@ -Supported cipher suites -======================= +Cipher suites supported by Corda +================================ .. contents:: diff --git a/docs/source/compatibility-zones.rst b/docs/source/compatibility-zones.rst index eb3e6680b2..0e94daaaa0 100644 --- a/docs/source/compatibility-zones.rst +++ b/docs/source/compatibility-zones.rst @@ -4,54 +4,29 @@ -Compatibility zones -=================== +What is a compatibility zone? +============================= Every Corda node is part of a "zone" (also sometimes called a Corda network) that is *permissioned*. Production -deployments require a secure certificate authority. Most users will join an existing network such as Corda -Network (the main network) or the Corda Testnet. We use the term "zone" to refer to a set of technically compatible nodes reachable -over a TCP/IP network like the internet. The word "network" is used in Corda but can be ambiguous with the concept -of a "business network", which is usually more like a membership list or subset of nodes in a zone that have agreed -to trade with each other. +deployments require a secure certificate authority. We use the term "zone" to refer to a set of technically compatible +nodes reachable over a TCP/IP network like the internet. The word "network" is used in Corda but can be ambiguous with +the concept of a "business network", which is usually more like a membership list or subset of nodes in a zone that +have agreed to trade with each other. -To connect to a compatibility zone you need to register with its certificate signing authority (doorman) by submitting -a certificate signing request (CSR) to obtain a valid identity for the zone. You could do this out of band, for instance -via email or a web form, but there's also a simple request/response protocol built into Corda. +How do I become part of a compatibility zone? +--------------------------------------------- -Before you can register, you must first have received the trust store file containing the root certificate from the zone -operator. For high security zones this might be delivered physically. Then run the following command: +Bootstrapping a compatibility zone +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +You can easily bootstrap a compatibility zone for testing or pre-production use with either the +:doc:`network-bootstrapper` or the :doc:`network-builder` tools. -``java -jar corda.jar --initial-registration --network-root-truststore-password `` +Joining an existing compatibility zone +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +After the testing and pre-production phases, users are encouraged to join an existing compatibility zone such as Corda +Network (the main compatibility zone) or the Corda Testnet. See :doc:`joining-a-compatibility-zone`. -By default it will expect the trust store file to be in the location ``certificates/network-root-truststore.jks``. -This can be overridden with the additional ``--network-root-truststore`` flag. - -The certificate signing request will be created based on node information obtained from the node configuration. -The following information from the node configuration file is needed to generate the request. - -* **myLegalName** Your company's legal name as an X.500 string. X.500 allows differentiation between entities with the same - name, as the legal name needs to be unique on the network. If another node has already been permissioned with this - name then the permissioning server will automatically reject the request. The request will also be rejected if it - violates legal name rules, see :ref:`node_naming` for more information. You can use the X.500 schema to disambiguate - entities that have the same or similar brand names. - -* **emailAddress** e.g. "admin@company.com" - -* **devMode** must be set to false - -* **networkServices** or **compatibilityZoneURL** The Corda compatibility zone services must be configured. This must be either: - - * **compatibilityZoneURL** The Corda compatibility zone network management service root URL. - * **networkServices** Replaces the ``compatibilityZoneURL`` when the doorman and network map services - are configured to operate on different URL endpoints. The ``doorman`` entry is used for registration. - -A new pair of private and public keys generated by the Corda node will be used to create the request. - -The utility will submit the request to the doorman server and poll for a result periodically to retrieve the -certificates. Once the request has been approved and the certificates downloaded from the server, the node will create -the keystore and trust store using the certificates and the generated private key. - -.. note:: You can exit the utility at any time if the approval process is taking longer than expected. The request - process will resume on restart as long as the ``--initial-registration`` flag is specified. - -This process only is needed when the node connects to the network for the first time, or when the certificate expires. +Setting up a dynamic compatibility zone +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Some users may also be interested in setting up their own dynamic compatibility zone. For instructions and a discussion +of whether this approach is suitable for you, see :doc:`setting-up-a-dynamic-compatibility-zone`. diff --git a/docs/source/corda-network/governance-guidelines.md b/docs/source/corda-network/governance-guidelines.md index 7d9adebc42..9c237c1ec8 100644 --- a/docs/source/corda-network/governance-guidelines.md +++ b/docs/source/corda-network/governance-guidelines.md @@ -1,5 +1,5 @@ Corda Network Foundation : Governance Guidelines -==================================================== +================================================ 23 October 2018 @@ -11,7 +11,7 @@ This is a set of governance guidelines for the Corda Network Foundation. It prov Board, to steer and govern Corda Network effectively to realise its potential. It is not a set of binding legal obligations. 1 Background to Corda and the Network -===================================== +------------------------------------- Corda allows multiple independent applications and private networks to coexist, each with their own business models and membership criteria, yet linked by the same underlying network (‘Corda Network’). This Network enables ‘interoperability’, @@ -23,7 +23,7 @@ specified in the Corda Open Source Project codebase, but later may be formalised which then will become canonical. 1.1 Reason for a Corda Network Foundation ------------------------------------------ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ R3 has set up and governs by default Corda Network currently (along with Corda). This includes making key decisions around establishing, maintaining and updating standards, policies, and procedures for participation in, and use of, Corda Network. @@ -41,10 +41,10 @@ In other words, to achieve the community's objective of Corda ubiquity, it is n structure which explicitly limits R3’s control of Corda Network, and enables this ubiquity. 2 The Corda Network Foundation -============================== +------------------------------ 2.1 Mission and Values ----------------------- +^^^^^^^^^^^^^^^^^^^^^^ Following on from the Corda introduction and technical white papers, we see the mission of the Corda Network Foundation to achieve the vision of Corda - whereby the state of transactions and agreements of business partners can be recorded in a single global database, ending the need for costly reconciliation and error correction, while maintaining privacy. @@ -85,7 +85,7 @@ developers, service and solution providers and end users. customers, open source developers, and R3’s shareholders. 2.2 Structure of the Foundation -------------------------------- +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The Foundation shall be a not-for-profit entity created exclusively to execute the mission set out in this Constitution. With the advice of international lawyers, this is a ‘Stichting’ domiciled in Holland – a legal entity suited for governance activities, able to act commercially, with limited liability but no shareholders, capital or dividends. @@ -112,10 +112,10 @@ and with privileges and responsibilities as set out in section 6. Any change to the structure of the Foundation is a constitutional change, described in section 5.1. 3 Governing Board -================= +----------------- 3.1 Role of the Board ---------------------- +^^^^^^^^^^^^^^^^^^^^^ The goal of the Board is primarily to ensure the stable and secure operation of the Network, as well as to achieve the vision of Corda laid out in section 2.1. The fundamental responsibility of directors appointed to the Board is to exercise their business judgement to act in what they believe to be the best interests of the Network, taking account @@ -129,7 +129,7 @@ The Board is the formal decision-making authority of the Foundation, and actions decision making. 3.2 Relationship of the Board with the Operator ------------------------------------------------ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ It is the duty of the Board to monitor the Operator’s performance to ensure that the Network operates in an effective, efficient and ethical manner. The Board will also be responsible for overseeing the Operator in the development of the Network’s strategic and tactical plans, ensuring that they will result in broad and open adoption of Corda. The Operator @@ -137,7 +137,7 @@ is responsible to the Board for the execution of day to day operations, and the change. 3.3 Composition and Establishment of the Board ----------------------------------------------- +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ### 3.3.1 Size The Board shall consist of 11 voting members (‘Directors’) in total, to allow broad representation but maintain an agile decision-making ability. The selection process (using the Participant Community) is intended to ensure that the Board is @@ -212,7 +212,7 @@ board meetings of more than six months, death, or if necessary, removal by a Man vacant seat will be contested at the next annual election. 3.4 Conduct of Board Meetings ------------------------------ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Attendance may be in person or by video-conference. The Board shall meet not less than every six months, and may meet on the request of any Director, but not more than every @@ -236,7 +236,7 @@ observer per unrepresented Participant. Observers may participate in discussions vote, and may be asked to join by video-conference if there are logistical constraints. 4 Relation of the Foundation to Business Networks -=================================================== +--------------------------------------------------- The global Network shall support the operation of any business networks which may be formed by industry-specific operators on top of the Network. The Board shall ensure that there is a clear separation between areas of governance @@ -247,14 +247,14 @@ under a Creative Commons license, both for reuse by business network operators i governance structure, and so that such governance layers are complementary and not contradictory. 5 Governance Events -===================== +--------------------- All formal changes to the Network and the Foundation shall be controlled through a formal Governance Event process, and the right to initiate this shall be held by all Directors and Participants. In the event of disruptive behaviour by an individual Participant or group of Participants, this right may be curtailed, as described in 5.2.5. 5.1 Types of Governance Events ------------------------------- +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ There are three types of change that affect the Network and the Foundation, which require a controlled change process and a vote described in 5.5, and are defined as Governance Events: @@ -280,7 +280,7 @@ Advisory Committee to provide due diligence and make a recommendation for implem For all Governance Events, decisions and the rationale for the decision shall be published transparently. 5.2 Mandatory Governance Events -------------------------------- +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ### 5.2.1 Access Standards The Corda system can be accessed by using software which implements the set of technical protocols which define compatibility (see 5.3.1) above). The reference implementation of this software is open source and freely accessible at @@ -347,7 +347,7 @@ Change to the arbitration and dispute resolution process shall be the subject of Policies covering areas of operation not covered by the Constitution (e.g. code of conduct for Board Directors). 5.3 Advisory Governance Events ------------------------------- +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #### 5.3.1 Technical Standards There is a set of technical standards, such as ‘network parameters’, which all Corda Network nodes need to comply with in order to guarantee technical compatibility to other nodes and services within the Network. While Corda has stability @@ -384,7 +384,7 @@ business network operator, or directly if no business network is involved. If ne the Board by creating an Advisory Governance Event. 5.4 Emergency Governance Events -------------------------------- +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Certain Network incidents, which could impact one or more Network participants and that would be the subject of Mandatory or Advisory Governance Events, shall require immediate resolution. In these cases, the Operator may make emergency changes, but these shall be subject to post-event evaluation and standard Governance Event processing. Areas @@ -392,7 +392,7 @@ of control that are the subject of Mandatory Governance Events are not expected the Operator shall be entitled to make emergency changes to preserve the stability and integrity of the Network. 5.5 Voting ----------- +^^^^^^^^^^ All Constitutional, Mandatory and Advisory Governance Events outlined in sections 5.2 and 5.3 shall be presented to the Board for voting. The representatives of the Board shall vote on a one vote per Director basis to approve or reject the Governance Event. @@ -416,10 +416,10 @@ in an attempt to simplify governance, provide transparency and lower costs, prov thoroughly and has sufficient manual override controls. 6 Participation -================= +----------------- 6.1 General Membership ----------------------- +^^^^^^^^^^^^^^^^^^^^^^ Participation is open to any potential participant on the Network, subject to meeting normal Network access conditions described in section 5.2.1, and paying a nominal annual participation fee to cover both the operational costs of Network services and the Foundation, and to ensure that its activities are sufficiently resourced. @@ -437,7 +437,7 @@ costs will depend on the individual event. 8. Use the Network for live business activities running 'in production'. 6.2 Technical Advisory Committee --------------------------------- +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The Technical Advisory Committee shall have limited participants appointed directly by the Board. Its mandate and charter will be set by the Board. It shall act directly on the instructions of the Board or the Operator, which shall set expected deliverables and timelines. It shall focus on specific technical topics and may have responsibility for @@ -452,7 +452,7 @@ the operation of the Network. elegant and practical system design 6.3 Governance Advisory Committee ---------------------------------- +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The Governance Advisory Committee shall have limited participants appointed directly by the board. Its purpose is to recommend actions to the Board for approval on non-technical matters, where additional support is helpful. This may include decisions on: @@ -464,7 +464,7 @@ include decisions on: 5. Complaints and Whistle-blowing 7 The Corda Network Operator -============================ +---------------------------- In order to pursue the mission of the Foundation as set out in section 1, there will need to be a set of operational activities, including technical activities such as hosting services, marketing activities, community management and @@ -480,10 +480,10 @@ have been designed to be highly cacheable, and low-cost in operation. For the first three years, R3 shall act as the Operator. 8 Costs and Participation Fees -============================== +------------------------------ 8.1 Costs ---------- +^^^^^^^^^ In line with the mission and values of the Foundation, the Network Foundation is not a profit seeking entity. But the Foundation needs to provide governance and technical services, and these will incur costs. The Foundation maintains these cost principles, as ideals but not contractual standards: @@ -496,7 +496,7 @@ of all of its own administration, governance and technical services. 5. The Foundation's cost model should be public, to demonstrate that the costs could not reasonably be lower. 8.2 Participation Fee ---------------------- +^^^^^^^^^^^^^^^^^^^^^ The Foundation shall meet costs by levying a participation fee and notary fee for all Participants. The participation fee will be independent of organisation size and number of transactions on Corda, to reflect the underlying cost of identity issuance. @@ -520,13 +520,13 @@ The fee applies even if the Participants chooses not to operate a Corda node on can be potential or active participants. 8.3 Notary Fee --------------- +^^^^^^^^^^^^^^ Transaction notary fees will be charged separately, on a per-use basis. This reflects the variable cost of providing notary services, with a wide orders-of-magnitude disparity between frequent and infrequent participant transaction volumes. As a principle, notary fees shall not subsidise participation fees, nor vice versa. 9 Community -=========== +----------- Corda is a collaborative effort, and part of the Foundation’s mission is to help create and foster a technical community that will benefit all Corda solution providers and users. As such, the Foundation will work to encourage further participation of leading Participants of the ecosystem, including developers, service and solution providers and end @@ -537,7 +537,7 @@ The Corda technical community should be broad and open, encouraging participatio technology and applications, but this cannot be mandated by the Foundation. 9.1 Non-Discrimination ----------------------- +^^^^^^^^^^^^^^^^^^^^^^ The Foundation will welcome any organization able to meet the Participation criteria, regardless of competitive interests with other Participants. The Board shall not seek to exclude any Participant for any reasons other than those that are reasonable, explicit and applied on a non-discriminatory basis to all Participants. diff --git a/docs/source/corda-networks-index.rst b/docs/source/corda-networks-index.rst index 2dbcea2a19..b57106cbb5 100644 --- a/docs/source/corda-networks-index.rst +++ b/docs/source/corda-networks-index.rst @@ -5,8 +5,6 @@ Networks :maxdepth: 1 compatibility-zones - corda-testnet-intro - running-a-notary permissioning network-map versioning @@ -14,3 +12,9 @@ Networks azure-template-guide testnet-explorer cipher-suites + joining-a-compatibility-zone + corda-testnet-intro + deploy-to-testnet-index + testnet-explorer-corda + setting-up-a-dynamic-compatibility-zone + running-a-notary diff --git a/docs/source/corda-testnet-intro.rst b/docs/source/corda-testnet-intro.rst index 370a710320..b32a2f5d4e 100644 --- a/docs/source/corda-testnet-intro.rst +++ b/docs/source/corda-testnet-intro.rst @@ -1,5 +1,5 @@ -The Corda Testnet -================= +Joining Corda Testnet +===================== .. contents:: diff --git a/docs/source/deploy-locally.rst b/docs/source/deploy-locally.rst new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/source/deploy-to-testnet-index.rst b/docs/source/deploy-to-testnet-index.rst new file mode 100644 index 0000000000..c17957b675 --- /dev/null +++ b/docs/source/deploy-to-testnet-index.rst @@ -0,0 +1,10 @@ +Deploying Corda to Testnet +========================== + +.. toctree:: + :maxdepth: 1 + + azure-vm-explore + aws-vm-explore + gcp-vm + deploy-locally diff --git a/docs/source/design/linear-pointer/design.md b/docs/source/design/linear-pointer/design.md new file mode 100644 index 0000000000..d19f695e99 --- /dev/null +++ b/docs/source/design/linear-pointer/design.md @@ -0,0 +1,144 @@ +# StatePointer + +## Background + +Occasionally there is a need to create a link from one `ContractState` to another. This has the effect of creating a uni-directional "one-to-one" relationship between a pair of `ContractState`s. + +There are two ways to do this. + +### By `StateRef` + +Link one `ContractState` to another by including a `StateRef` or a `StateAndRef` as a property inside another `ContractState`: + +```kotlin +// StateRef. +data class FooState(val ref: StateRef) : ContractState +// StateAndRef. +data class FooState(val ref: StateAndRef) : ContractState +``` + +Linking to a `StateRef` or `StateAndRef` is only recommended if a specific version of a state is required in perpetuity. Clearly, adding a `StateAndRef` embeds the data directly. This type of pointer is compatible with any `ContractState` type. + +But what if the linked state is updated? The `StateRef` will be pointing to an older version of the data and this could be a problem for the `ContractState` which contains the pointer. + +### By `linearId` + +To create a link to the most up-to-date version of a state, instead of linking to a specific `StateRef`, a `linearId` which references a `LinearState` can be used. This is because all `LinearState`s contain a `linearId` which refers to a particular lineage of `LinearState`. The vault can be used to look-up the most recent state with the specified `linearId`. + +```kotlin +// Link by LinearId. +data class FooState(val ref: UniqueIdentifier) : ContractState +``` + +This type of pointer only works with `LinearState`s. + +### Resolving pointers + +The trade-off with pointing to data in another state is that the data being pointed to cannot be immediately seen. To see the data contained within the pointed-to state, it must be "resolved". + +## Design + +Introduce a `StatePointer` interface and two implementations of it; the `StaticPointer` and the `LinearPointer`. The `StatePointer` is defined as follows: + +```kotlin +interface StatePointer { + val pointer: Any + fun resolve(services: ServiceHub): StateAndRef +} +``` + +The `resolve` method facilitates the resolution of the `pointer` to a `StateAndRef`. + +The `StaticPointer` type requires developers to provide a `StateRef` which points to a specific state. + +```kotlin +class StaticPointer(override val pointer: StateRef) : StatePointer { + override fun resolve(services: ServiceHub): StateAndRef { + val transactionState = services.loadState(pointer) + return StateAndRef(transactionState, pointer) + } +} +``` + +The `LinearPointer` type contains the `linearId` of the `LinearState` being pointed to and a `resolve` method. Resolving a `LinearPointer` returns a `StateAndRef` containing the latest version of the `LinearState` that the node calling `resolve` is aware of. + +```kotlin +class LinearPointer(override val pointer: UniqueIdentifier) : StatePointer { + override fun resolve(services: ServiceHub): StateAndRef { + val query = QueryCriteria.LinearStateQueryCriteria(linearId = listOf(pointer)) + val result = services.vaultService.queryBy(query).states + check(result.isNotEmpty()) { "LinearPointer $pointer cannot be resolved." } + return result.single() + } +} +``` + +#### Bi-directional link + +Symmetrical relationships can be modelled by embedding a `LinearPointer` in the pointed-to `LinearState` which points in the "opposite" direction. **Note:** this can only work if both states are `LinearState`s. + +## Use-cases + +It is important to note that this design only standardises a pattern which is currently possible with the platform. In other words, this design does not enable anything new. + +#### Tokens + +Uncoupling token type definitions from the notion of ownership. Using the `LinearPointer`, `Token` states can include an `Amount` of some pointed-to type. The pointed-to type can evolve independently from the `Token` state which should just be concerned with the question of ownership. + +## Issues and resolutions + +Some issue to be aware of and their resolutions: + +| Problem | Resolution | +| :----------------------------------------------------------- | ------------------------------------------------------------ | +| If the node calling `resolve` has not seen the specified `StateRef`, then `resolve` will return `null`. Here, the node calling `resolve` might be missing some crucial data. | Use data distribution groups. Assuming the creator of the `ContractState` publishes it to a data distribution group, subscribing to that group ensures that the node calling resolve will eventually have the required data. | +| The node calling `resolve` has seen and stored transactions containing a `LinearState` with the specified `linearId`. However, there is no guarantee the `StateAndRef` returned by `resolve` is the most recent version of the `LinearState`. | Embed the pointed-to `LinearState` in transactions containing the `LinearPointer` as a reference state. The reference states feature will ensure the pointed-to state is the latest version. | +| The creator of the pointed-to `ContractState` exits the state from the ledger. If the pointed-to state is included a reference state then notaries will reject transactions containing it. | Contract code can be used to make a state un-exitable. | + +All of the noted resolutions rely on additional paltform features: + +* Reference states which will be available in V4 +* Data distribution groups which are not currently available. However, there is an early prototype +* Additional state interface + +### Additional concerns and responses + +#### Embedding reference states in transactions + +**Concern:** Embedding reference states for pointed-to states in transactions could cause transactions to increase by some unbounded size. + +**Response:** The introduction of this feature doesn't create a new platform capability. It merely formalises a pattern which is currently possible. Futhermore, there is a possibility that _any_ type of state can cause a transaction to increase by some un-bounded size. It is also worth remembering that the maximum transaction size is 10MB. + +#### `StatePointer`s are not human readable + +**Concern:** Users won't know what sits behind the pointer. + +**Response:** When the state containing the pointer is used in a flow, the pointer can be easily resolved. When the state needs to be displayed on a UI, the pointer can be resolved via vault query. + +#### This feature adds complexity to the platform + +**Concern:** This all seems quite complicated. + +**Response:** It's possible anyway. Use of this feature is optional. + +#### Coinselection will be slow. + +**Concern:** We'll need to join on other tables to perform coinselection, making it slower. This is when a `StatePointer` is used as a `FungibleState` or `FungibleAsset` type. + +**Response:** This is probably not true in most cases. Take the existing coinselection code from `CashSelectionH2Impl.kt`: + +```sql +SELECT vs.transaction_id, vs.output_index, ccs.pennies, SET(@t, ifnull(@t,0)+ccs.pennies) total_pennies, vs.lock_id +FROM vault_states AS vs, contract_cash_states AS ccs +WHERE vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index +AND vs.state_status = 0 +AND vs.relevancy_status = 0 +AND ccs.ccy_code = ? and @t < ? +AND (vs.lock_id = ? OR vs.lock_id is null) +``` + +Notice that the only property required which is not accessible from the `StatePointer` is the `ccy_code`. This is not necessarily a problem though, as the `pointer` specified in the pointer can be used as a proxy for the `ccy_code` or "token type". + + + + diff --git a/docs/source/joining-a-compatibility-zone.rst b/docs/source/joining-a-compatibility-zone.rst new file mode 100644 index 0000000000..72df1ee193 --- /dev/null +++ b/docs/source/joining-a-compatibility-zone.rst @@ -0,0 +1,58 @@ +.. highlight:: kotlin +.. raw:: html + + + + +Joining an existing compatibility zone +====================================== + +To connect to a compatibility zone you need to register with its certificate signing authority (or *doorman*) by +submitting a certificate signing request (CSR) to obtain a valid identity for the zone. This process is only necessary +when the node connects to the network for the first time, or when the certificate expires. You could do this out of +band, for instance via email or a web form, but there's also a simple request/response utility built into the node. + +Before using this utility, you must first have received the trust store file containing the root certificate from the +zone operator. For high security zones, this might be delivered physically. Then run the following command: + +``java -jar corda.jar --initial-registration --network-root-truststore-password `` + +By default, the utility expects the trust store file to be in the location ``certificates/network-root-truststore.jks``. +This can be overridden using the additional ``--network-root-truststore`` flag. + +The utility performs the following steps: + +1. It creates a certificate signing request based on the following information from the node's configuration file (see + :doc:`corda-configuration-file`): + + * **myLegalName** Your company's legal name as an X.500 string. X.500 allows differentiation between entities with the same + name, as the legal name needs to be unique on the network. If another node has already been permissioned with this + name then the permissioning server will automatically reject the request. The request will also be rejected if it + violates legal name rules, see :ref:`node_naming` for more information. You can use the X.500 schema to disambiguate + entities that have the same or similar brand names + + * **emailAddress** e.g. "admin@company.com" + + * **devMode** must be set to false + + * **compatibilityZoneURL** or **networkServices** The address(es) used to register with the compatibility zone and + retrieve the network map. These should be provided to you by the operator of the zone. This must be either: + + * **compatibilityZoneURL** The root address of the network management service. Use this if both the doorman and the + network map service are operating on the same URL endpoint + * **networkServices** The root addresses of the doorman and the network map service. Use this if the doorman and the + network map service are operating on the same URL endpoint, where: + + * **doormanURL** is the root address of the doorman. This is the address used for initial registration + * **networkMapURL** is the root address of the network map service + +2. It generates a new private/public keypair to sign the certificate signing request + +3. It submits the request to the doorman server and polls periodically to retrieve the corresponding certificates + +4. It creates the node's keystore and trust store using the received certificates + +5. It creates and stores the node's TLS keys and legal identity key along with their corresponding certificate-chains + +.. note:: You can exit the utility at any time if the approval process is taking longer than expected. The request + process will resume on restart as long as the ``--initial-registration`` flag is specified. diff --git a/docs/source/network-bootstrapper.rst b/docs/source/network-bootstrapper.rst index d8568e7ca9..a8bc5b0c97 100644 --- a/docs/source/network-bootstrapper.rst +++ b/docs/source/network-bootstrapper.rst @@ -247,6 +247,54 @@ To give the following: .. note:: The whitelist can only ever be appended to. Once added a contract implementation can never be removed. +Package namespace ownership +---------------------------- + +Package namespace ownership is a Corda security feature that allows a compatibility zone to give ownership of parts of the Java package namespace to registered users (e.g. CorDapp development organisations). +The exact mechanism used to claim a namespace is up to the zone operator. A typical approach would be to accept an SSL +certificate with the domain in it as proof of domain ownership, or to accept an email from that domain. + +.. note:: Read more about *Package ownership* :doc:`here`. + +A Java package namespace is case insensitive and cannot be a sub-package of an existing registered namespace. +See `Naming a Package `_ and `Naming Conventions `_ for guidelines on naming conventions. + +Registration of a java package namespace requires creation of a signed certificate as generated by the +`Java keytool `_. + +The following four items are passed as a semi-colon separated string to the ``--register-package-owner`` command: + + 1. Java package name (e.g `com.my_company` ). + 2. Keystore file refers to the full path of the file containing the signed certificate. + 3. Password refers to the key store password (not to be confused with the key password). + 4. Alias refers to the name associated with a certificate containing the public key to be associated with the package namespace. + +Let's use the `Example CorDapp `_ to initialise a simple network, and then register and unregister a package namespace. +Checkout the Example CorDapp and follow the instructions to build it `here `_. + +.. note:: You can point to any existing bootstrapped corda network (this will have the effect of updating the associated network parameters file). + +1. Create a new public key to use for signing the java package namespace we wish to register: + +.. code-block:: shell + + $JAVA_HOME/bin/keytool -genkeypair -keystore _teststore -storepass MyStorePassword -keyalg RSA -alias MyKeyAlias -keypass MyKeyPassword -dname "O=Alice Corp, L=Madrid, C=ES" + +This will generate a key store file called ``_teststore`` in the current directory. + +2. Register the package namespace to be claimed by the public key generated above: + +.. code-block:: shell + + # Register the java package namespace using the bootstrapper tool + java -jar network-bootstrapper.jar --dir build/nodes --register-package-owner com.example;./_teststore;MyStorePassword;MyKeyAlias + +3. Unregister the package namespace: + +.. code-block:: shell + + # Unregister the java package namespace using the bootstrapper tool + java -jar network-bootstrapper.jar --dir build/nodes --unregister-package-owner com.example Command-line options -------------------- @@ -256,7 +304,10 @@ The network bootstrapper can be started with the following command-line options: .. code-block:: shell bootstrapper [-hvV] [--no-copy] [--dir=] [--logging-level=] - [--minimum-platform-version=] [COMMAND] + [--minimum-platform-version=] + [--register-package-owner java-package-namespace=keystore-file:password:alias] + [--unregister-package-owner java-package-namespace] + [COMMAND] * ``--dir=``: Root directory containing the node configuration files and CorDapp JARs that will form the test network. It may also contain existing node directories. Defaults to the current directory. @@ -266,8 +317,11 @@ The network bootstrapper can be started with the following command-line options: * ``--help``, ``-h``: Show this help message and exit. * ``--version``, ``-V``: Print version information and exit. * ``--minimum-platform-version``: The minimum platform version to use in the generated network-parameters. +* ``--register-package-owner``: Register a java package namespace with its owners public key. +* ``--unregister-package-owner``: Unregister a java package namespace. Sub-commands ^^^^^^^^^^^^ -``install-shell-extensions``: Install ``bootstrapper`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info. \ No newline at end of file +``install-shell-extensions``: Install ``bootstrapper`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info. + diff --git a/docs/source/network-map.rst b/docs/source/network-map.rst index 7263d8d089..28a19cd0d8 100644 --- a/docs/source/network-map.rst +++ b/docs/source/network-map.rst @@ -1,5 +1,5 @@ -Network map -=========== +The network map +=============== .. contents:: @@ -45,6 +45,8 @@ The set of REST end-points for the network map service are as follows. +----------------+-----------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------+ | GET | /network-map/network-parameters/{hash} | Retrieve the signed network parameters (see below). The entire object is signed with the network map certificate which is also attached. | +----------------+-----------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------+ +| GET | /network-map/my-hostname | Retrieve the IP address of the caller (and **not** of the network map). | ++----------------+-----------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------+ HTTP is used for the network map service instead of Corda's own AMQP based peer to peer messaging protocol to enable the server to be placed behind caching content delivery networks like Cloudflare, Akamai, Amazon Cloudfront and so on. diff --git a/docs/source/permissioning.rst b/docs/source/permissioning.rst index c7ca9986cc..2e12ed7524 100644 --- a/docs/source/permissioning.rst +++ b/docs/source/permissioning.rst @@ -4,8 +4,8 @@ -Network permissioning -===================== +Network certificates +==================== .. contents:: diff --git a/docs/source/setting-up-a-dynamic-compatibility-zone.rst b/docs/source/setting-up-a-dynamic-compatibility-zone.rst new file mode 100644 index 0000000000..be5b5f7dc4 --- /dev/null +++ b/docs/source/setting-up-a-dynamic-compatibility-zone.rst @@ -0,0 +1,191 @@ +.. highlight:: kotlin +.. raw:: html + + + + +Setting up a dynamic compatibility zone +======================================= + +.. contents:: + +Do you need to create your own dynamic compatibility zone? +---------------------------------------------------------- + +By *dynamic compatibility zone*, we mean a compatibility zone that relies on a network map server to allow nodes to +join dynamically, instead of requiring each node to be bootstrapped and have the node-infos distributed manually. While +this may sound appealing, think twice before going down this route: + +1. If you need to test a CorDapp, it is easier to create a test network using the network bootstrapper tool (see below) +2. If you need to control who uses your CorDapp, it is easier to apply permissioning by creating a business network + (see below) + +**Testing.** Creating a production-ready zone isn't necessary for testing as you can use the *network bootstrapper* +tool to create all the certificates, keys, and distribute the needed map files to run many nodes. The bootstrapper can +create a network locally on your desktop/laptop but it also knows how to automate cloud providers via their APIs and +using Docker. In this way you can bring up a simulation of a real Corda network with different nodes on different +machines in the cloud for your own testing. Testing this way has several advantages, most obviously that you avoid +race conditions in your tests caused by nodes/tests starting before all map data has propagated to all nodes. +You can read more about the reasons for the creation of the bootstrapper tool +`in a blog post on the design thinking behind Corda's network map infrastructure `__. + +**Permissioning.** And creating a zone is also unnecessary for imposing permissioning requirements beyond that of the +base Corda network. You can control who can use your app by creating a *business network*. A business network is what we +call a coalition of nodes that have chosen to run a particular app within a given commercial context. Business networks +aren't represented in the Corda API at this time, partly because the technical side is so simple. You can create one +via a simple three step process: + +1. Distribute a list of X.500 names that are members of your business network. You can use the + `reference Business Network Membership Service implementation `_. + Alternatively, you could do this is by hosting a text file with one name per line on your website at a fixed HTTPS + URL. You could also write a simple request/response flow that serves the list over the Corda protocol itself, + although this requires the business network to have its own node. +2. Write a bit of code that downloads and caches the contents of this file on disk, and which loads it into memory in + the node. A good place to do this is in a class annotated with ``@CordaService``, because this class can expose + a ``Set`` field representing the membership of your service. +3. In your flows use ``serviceHub.findService`` to get a reference to your ``@CordaService`` class, read the list of + members and at the start of each flow, throw a FlowException if the counterparty isn't in the membership list. + +In this way you can impose a centrally controlled ACL that all members will collectively enforce. + +.. note:: A production-ready Corda network and a new iteration of the testnet will be available soon. + +Why create your own zone? +------------------------- + +The primary reason to create a zone and provide the associated infrastructure is control over *network parameters*. These +are settings that control Corda's operation, and on which all users in a network must agree. Failure to agree would create +the Corda equivalent of a blockchain "hard fork". Parameters control things like the root of identity, +how quickly users should upgrade, how long nodes can be offline before they are evicted from the system and so on. + +Creating a zone involves the following steps: + +1. Create the zone private keys and certificates. This procedure is conventional and no special knowledge is required: + any self-signed set of certificates can be used. A professional quality zone will probably keep the keys inside a + hardware security module (as the main Corda network and test networks do). +2. Write a network map server. +3. Optionally, create a doorman server. +4. Finally, you would select and generate your network parameter file. + +How to create your own compatibility zone +----------------------------------------- + +Using an existing network map implementation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +You can use an existing network map implementation such as the +`Cordite Network Map Service `_ to create a dynamic compatibility zone. + +Creating your own network map implementation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Writing a network map server +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This server implements a simple HTTP based protocol described in the ":doc:`network-map`" page. +The map server is responsible for gathering NodeInfo files from nodes, storing them, and distributing them back to the +nodes in the zone. By doing this it is also responsible for choosing who is in and who is out: having a signed +identity certificate is not enough to be a part of a Corda zone, you also need to be listed in the network map. +It can be thought of as a DNS equivalent. If you want to de-list a user, you would do it here. + +It is very likely that your map server won't be entirely standalone, but rather, integrated with whatever your master +user database is. + +The network map server also distributes signed network parameter files and controls the rollout schedule for when they +become available for download and opt-in, and when they become enforced. This is again a policy decision you will +probably choose to place some simple UI or workflow tooling around, in particular to enforce restrictions on who can +edit the map or the parameters. + +Writing a doorman server +~~~~~~~~~~~~~~~~~~~~~~~~ + +This step is optional because your users can obtain a signed certificate in many different ways. The doorman protocol +is again a very simple HTTP based approach in which a node creates keys and requests a certificate, polling until it +gets back what it expects. However, you could also integrate this process with the rest of your signup process. For example, +by building a tool that's integrated with your payment flow (if payment is required to take part in your zone at all). +Alternatively you may wish to distribute USB smartcard tokens that generate the private key on first use, as is typically +seen in national PKIs. There are many options. + +If you do choose to make a doorman server, the bulk of the code you write will be workflow related. For instance, +related to keeping track of an applicant as they proceed through approval. You should also impose any naming policies +you have in the doorman process. If names are meant to match identities registered in government databases then that +should be enforced here, alternatively, if names can be self-selected or anonymous, you would only bother with a +deduplication check. Again it will likely be integrated with a master user database. + +Corda does not currently provide a doorman or network map service out of the box, partly because when stripped of the +zone specific policy there isn't much to them: just a basic HTTP server that most programmers will have favourite +frameworks for anyway. + +The protocol is: + +* If $URL = ``https://some.server.com/some/path`` +* Node submits a PKCS#10 certificate signing request using HTTP POST to ``$URL/certificate``. It will have a MIME + type of ``application/octet-stream``. The ``Client-Version`` header is set to be "1.0". +* The server returns an opaque string that references this request (let's call it ``$requestid``, or an HTTP error if something went wrong. +* The returned request ID should be persisted to disk, to handle zones where approval may take a long time due to manual + intervention being required. +* The node starts polling ``$URL/$requestid`` using HTTP GET. The poll interval can be controlled by the server returning + a response with a ``Cache-Control`` header. +* If the request is answered with a ``200 OK`` response, the body is expected to be a zip file. Each file is expected to + be a binary X.509 certificate, and the certs are expected to be in order. +* If the request is answered with a ``204 No Content`` response, the node will try again later. +* If the request is answered with a ``403 Not Authorized`` response, the node will treat that as request rejection and give up. +* Other response codes will cause the node to abort with an exception. + +Setting zone parameters +~~~~~~~~~~~~~~~~~~~~~~~ + +Zone parameters are stored in a file containing a Corda AMQP serialised ``SignedDataWithCert`` +object. It is easy to create such a file with a small Java or Kotlin program. The ``NetworkParameters`` object is a +simple data holder that could be read from e.g. a config file, or settings from a database. Signing and saving the +resulting file is just a few lines of code. A full example can be found in ``NetworkParametersCopier.kt`` in the source +tree, but a flavour of it looks like this: + +.. container:: codeset + + .. sourcecode:: java + + NetworkParameters networkParameters = new NetworkParameters( + 4, // minPlatformVersion + Collections.emptyList(), // the `NotaryInfo`s of all the network's notaries + 1024 * 1024 * 20, // maxMessageSize + 1024 * 1024 * 15, // maxTransactionSize + Instant.now(), // modifiedTime + 2, // epoch + Collections.emptyMap() // whitelisted contract code JARs + ); + CertificateAndKeyPair signingCertAndKeyPair = loadNetworkMapCA(); + SerializedBytes> bytes = SerializedBytes.from(netMapCA.sign(networkParameters)); + Files.copy(bytes.open(), Paths.get("params-file")); + + .. sourcecode:: kotlin + + val networkParameters = NetworkParameters( + minimumPlatformVersion = 4, + notaries = listOf(...), + maxMessageSize = 1024 * 1024 * 20 // 20mb, for example. + maxTransactionSize = 1024 * 1024 * 15, + modifiedTime = Instant.now(), + epoch = 2, + ... etc ... + ) + val signingCertAndKeyPair: CertificateAndKeyPair = loadNetworkMapCA() + val signedParams: SerializedBytes = signingCertAndKeyPair.sign(networkParameters).serialize() + signedParams.open().copyTo(Paths.get("/some/path")) + +Each individual parameter is documented in `the JavaDocs/KDocs for the NetworkParameters class +`__. The network map +certificate is usually chained off the root certificate, and can be created according to the instructions above. Each +time the zone parameters are changed, the epoch should be incremented. Epochs are essentially version numbers for the +parameters, and they therefore cannot go backwards. Once saved, the new parameters can be served by the network map server. + +Selecting parameter values +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +How to choose the parameters? This is the most complex question facing you as a new zone operator. Some settings may seem +straightforward and others may involve cost/benefit tradeoffs specific to your business. For example, you could choose +to run a validating notary yourself, in which case you would (in the absence of SGX) see all the users' data. Or you could +run a non-validating notary, with BFT fault tolerance, which implies recruiting others to take part in the cluster. + +New network parameters will be added over time as Corda evolves. You will need to ensure that when your users upgrade, +all the new network parameters are being served. You can ask for advice on the `corda-dev mailing list `__. diff --git a/docs/source/tutorial-attachments.rst b/docs/source/tutorial-attachments.rst index 1956c9331a..856fc78559 100644 --- a/docs/source/tutorial-attachments.rst +++ b/docs/source/tutorial-attachments.rst @@ -50,9 +50,9 @@ a JVM client. Searching for attachments ------------------------- -Attachments metadata can be used to query, in the similar manner as :doc:`api-vault-query`. +Attachment metadata can be queried in a similar way to the vault (see :doc:`api-vault-query`). -``AttachmentQueryCriteria`` can be used to build a query, utilizing set of operations per column, namely: +``AttachmentQueryCriteria`` can be used to build a query using the following set of column operations: * Binary logical (AND, OR) * Comparison (LESS_THAN, LESS_THAN_OR_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL) @@ -61,11 +61,11 @@ Attachments metadata can be used to query, in the similar manner as :doc:`api-va * Nullability (IS_NULL, NOT_NULL) * Collection based (IN, NOT_IN) -``And`` and ``or`` operators can be used to build queries of arbitrary complexity. For example: +The ``and`` and ``or`` operators can be used to build complex queries. For example: .. container:: codeset - .. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt + .. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt :language: kotlin :start-after: DOCSTART AttachmentQueryExample1 :end-before: DOCEND AttachmentQueryExample1 diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt index f9ed9e0f88..db539a2c4d 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt @@ -2,11 +2,13 @@ package net.corda.nodeapi.internal.network import com.typesafe.config.Config import com.typesafe.config.ConfigFactory +import net.corda.core.crypto.toStringShort import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.* import net.corda.core.internal.concurrent.fork import net.corda.core.internal.concurrent.transpose +import net.corda.core.node.JavaPackageName import net.corda.core.node.NetworkParameters import net.corda.core.node.NodeInfo import net.corda.core.node.NotaryInfo @@ -17,6 +19,7 @@ import net.corda.core.serialization.deserialize import net.corda.core.serialization.internal.SerializationEnvironment import net.corda.core.serialization.internal._contextSerializationEnv import net.corda.core.utilities.days +import net.corda.core.utilities.filterNotNullValues import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.seconds import net.corda.nodeapi.internal.* @@ -30,6 +33,7 @@ import net.corda.serialization.internal.amqp.amqpMagic import java.io.InputStream import java.nio.file.Path import java.nio.file.StandardCopyOption.REPLACE_EXISTING +import java.security.PublicKey import java.time.Instant import java.util.* import java.util.concurrent.Executors @@ -168,14 +172,14 @@ internal constructor(private val initSerEnv: Boolean, } /** Entry point for the tool */ - fun bootstrap(directory: Path, copyCordapps: Boolean, minimumPlatformVersion: Int) { + fun bootstrap(directory: Path, copyCordapps: Boolean, minimumPlatformVersion: Int, packageOwnership : Map = emptyMap()) { require(minimumPlatformVersion <= PLATFORM_VERSION) { "Minimum platform version cannot be greater than $PLATFORM_VERSION" } // Don't accidently include the bootstrapper jar as a CorDapp! val bootstrapperJar = javaClass.location.toPath() val cordappJars = directory.list { paths -> paths.filter { it.toString().endsWith(".jar") && !it.isSameAs(bootstrapperJar) && it.fileName.toString() != "corda.jar" }.toList() } - bootstrap(directory, cordappJars, copyCordapps, fromCordform = false, minimumPlatformVersion = minimumPlatformVersion) + bootstrap(directory, cordappJars, copyCordapps, fromCordform = false, minimumPlatformVersion = minimumPlatformVersion, packageOwnership = packageOwnership) } private fun bootstrap( @@ -183,7 +187,8 @@ internal constructor(private val initSerEnv: Boolean, cordappJars: List, copyCordapps: Boolean, fromCordform: Boolean, - minimumPlatformVersion: Int = PLATFORM_VERSION + minimumPlatformVersion: Int = PLATFORM_VERSION, + packageOwnership : Map = emptyMap() ) { directory.createDirectories() println("Bootstrapping local test network in $directory") @@ -223,7 +228,7 @@ internal constructor(private val initSerEnv: Boolean, val notaryInfos = gatherNotaryInfos(nodeInfoFiles, configs) println("Generating contract implementations whitelist") val newWhitelist = generateWhitelist(existingNetParams, readExcludeWhitelist(directory), cordappJars.filter { !isSigned(it) }.map(contractsJarConverter)) - val newNetParams = installNetworkParameters(notaryInfos, newWhitelist, existingNetParams, nodeDirs, minimumPlatformVersion) + val newNetParams = installNetworkParameters(notaryInfos, newWhitelist, existingNetParams, nodeDirs, minimumPlatformVersion, packageOwnership) if (newNetParams != existingNetParams) { println("${if (existingNetParams == null) "New" else "Updated"} $newNetParams") } else { @@ -355,17 +360,31 @@ internal constructor(private val initSerEnv: Boolean, whitelist: Map>, existingNetParams: NetworkParameters?, nodeDirs: List, - minimumPlatformVersion: Int + minimumPlatformVersion: Int, + packageOwnership : Map ): NetworkParameters { - // TODO Add config for minimumPlatformVersion, maxMessageSize and maxTransactionSize + // TODO Add config for maxMessageSize and maxTransactionSize val netParams = if (existingNetParams != null) { - if (existingNetParams.whitelistedContractImplementations == whitelist && existingNetParams.notaries == notaryInfos) { + if (existingNetParams.whitelistedContractImplementations == whitelist && existingNetParams.notaries == notaryInfos && + existingNetParams.packageOwnership.entries.containsAll(packageOwnership.entries)) { existingNetParams } else { + var updatePackageOwnership = mutableMapOf(*existingNetParams.packageOwnership.map { Pair(it.key,it.value) }.toTypedArray()) + packageOwnership.forEach { key, value -> + if (value == null) { + if (updatePackageOwnership.remove(key) != null) + println("Unregistering package $key") + } + else { + if (updatePackageOwnership.put(key, value) == null) + println("Registering package $key for owner ${value.toStringShort()}") + } + } existingNetParams.copy( notaries = notaryInfos, modifiedTime = Instant.now(), whitelistedContractImplementations = whitelist, + packageOwnership = updatePackageOwnership, epoch = existingNetParams.epoch + 1 ) } @@ -377,6 +396,7 @@ internal constructor(private val initSerEnv: Boolean, maxMessageSize = 10485760, maxTransactionSize = 10485760, whitelistedContractImplementations = whitelist, + packageOwnership = packageOwnership.filterNotNullValues(), epoch = 1, eventHorizon = 30.days ) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt index a3ddb3cc93..0c1b439d99 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt @@ -5,29 +5,28 @@ import net.corda.core.crypto.secureRandomBytes import net.corda.core.crypto.sha256 import net.corda.core.identity.CordaX500Name import net.corda.core.internal.* +import net.corda.core.node.JavaPackageName import net.corda.core.node.NetworkParameters import net.corda.core.node.NodeInfo import net.corda.core.serialization.serialize import net.corda.node.services.config.NotaryConfig import net.corda.nodeapi.internal.DEV_ROOT_CA import net.corda.nodeapi.internal.NODE_INFO_DIRECTORY -import net.corda.core.internal.PLATFORM_VERSION import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.config.parseAs import net.corda.nodeapi.internal.config.toConfig import net.corda.nodeapi.internal.network.NodeInfoFilesCopier.Companion.NODE_INFO_FILE_NAME_PREFIX -import net.corda.testing.core.ALICE_NAME -import net.corda.testing.core.BOB_NAME -import net.corda.testing.core.DUMMY_NOTARY_NAME -import net.corda.testing.core.SerializationEnvironmentRule +import net.corda.testing.core.* import net.corda.testing.internal.createNodeInfoAndSigned import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.After import org.junit.Rule import org.junit.Test +import org.junit.rules.ExpectedException import org.junit.rules.TemporaryFolder import java.nio.file.Path +import java.security.PublicKey import kotlin.streams.toList class NetworkBootstrapperTest { @@ -35,6 +34,10 @@ class NetworkBootstrapperTest { @JvmField val tempFolder = TemporaryFolder() + @Rule + @JvmField + val expectedEx: ExpectedException = ExpectedException.none() + @Rule @JvmField val testSerialization = SerializationEnvironmentRule() @@ -208,6 +211,80 @@ class NetworkBootstrapperTest { assertThat(networkParameters.epoch).isEqualTo(2) } + private val ALICE = TestIdentity(ALICE_NAME, 70) + private val BOB = TestIdentity(BOB_NAME, 80) + + private val alicePackageName = JavaPackageName("com.example.alice") + private val bobPackageName = JavaPackageName("com.example.bob") + + @Test + fun `register new package namespace in existing network`() { + createNodeConfFile("alice", aliceConfig) + bootstrap(packageOwnership = mapOf(Pair(alicePackageName, ALICE.publicKey))) + assertContainsPackageOwner("alice", mapOf(Pair(alicePackageName, ALICE.publicKey))) + } + + @Test + fun `register additional package namespace in existing network`() { + createNodeConfFile("alice", aliceConfig) + bootstrap(packageOwnership = mapOf(Pair(alicePackageName, ALICE.publicKey))) + assertContainsPackageOwner("alice", mapOf(Pair(alicePackageName, ALICE.publicKey))) + // register additional package name + createNodeConfFile("bob", bobConfig) + bootstrap(packageOwnership = mapOf(Pair(bobPackageName, BOB.publicKey))) + assertContainsPackageOwner("bob", mapOf(Pair(alicePackageName, ALICE.publicKey), Pair(bobPackageName, BOB.publicKey))) + } + + @Test + fun `attempt to register overlapping namespaces in existing network`() { + createNodeConfFile("alice", aliceConfig) + val greedyNamespace = JavaPackageName("com.example") + bootstrap(packageOwnership = mapOf(Pair(greedyNamespace, ALICE.publicKey))) + assertContainsPackageOwner("alice", mapOf(Pair(greedyNamespace, ALICE.publicKey))) + // register overlapping package name + createNodeConfFile("bob", bobConfig) + expectedEx.expect(IllegalArgumentException::class.java) + expectedEx.expectMessage("multiple packages added to the packageOwnership overlap.") + bootstrap(packageOwnership = mapOf(Pair(bobPackageName, BOB.publicKey))) + } + + @Test + fun `unregister single package namespace in network of one`() { + createNodeConfFile("alice", aliceConfig) + bootstrap(packageOwnership = mapOf(Pair(alicePackageName, ALICE.publicKey))) + assertContainsPackageOwner("alice", mapOf(Pair(alicePackageName, ALICE.publicKey))) + // unregister package name + bootstrap(packageOwnership = mapOf(Pair(alicePackageName, null))) + assertContainsPackageOwner("alice", emptyMap()) + } + + @Test + fun `unregister single package namespace in network of many`() { + createNodeConfFile("alice", aliceConfig) + bootstrap(packageOwnership = mapOf(Pair(alicePackageName, ALICE.publicKey), Pair(bobPackageName, BOB.publicKey))) + // unregister package name + bootstrap(packageOwnership = mapOf(Pair(alicePackageName, null))) + assertContainsPackageOwner("alice", mapOf(Pair(bobPackageName, BOB.publicKey))) + } + + @Test + fun `unregister all package namespaces in existing network`() { + createNodeConfFile("alice", aliceConfig) + bootstrap(packageOwnership = mapOf(Pair(alicePackageName, ALICE.publicKey), Pair(bobPackageName, BOB.publicKey))) + // unregister all package names + bootstrap(packageOwnership = mapOf(Pair(alicePackageName, null), Pair(bobPackageName, null))) + assertContainsPackageOwner("alice", emptyMap()) + } + + @Test + fun `register and unregister sample package namespace in network`() { + createNodeConfFile("alice", aliceConfig) + bootstrap(packageOwnership = mapOf(Pair(alicePackageName, ALICE.publicKey), Pair(alicePackageName, null))) + assertContainsPackageOwner("alice", emptyMap()) + bootstrap(packageOwnership = mapOf(Pair(alicePackageName, null), Pair(alicePackageName, ALICE.publicKey))) + assertContainsPackageOwner("alice", mapOf(Pair(alicePackageName, ALICE.publicKey))) + } + private val rootDir get() = tempFolder.root.toPath() private fun fakeFileBytes(writeToFile: Path? = null): ByteArray { @@ -216,9 +293,9 @@ class NetworkBootstrapperTest { return bytes } - private fun bootstrap(copyCordapps: Boolean = true) { + private fun bootstrap(copyCordapps: Boolean = true, packageOwnership : Map = emptyMap()) { providedCordaJar = (rootDir / "corda.jar").let { if (it.exists()) it.readAll() else null } - bootstrapper.bootstrap(rootDir, copyCordapps, PLATFORM_VERSION) + bootstrapper.bootstrap(rootDir, copyCordapps, PLATFORM_VERSION, packageOwnership) } private fun createNodeConfFile(nodeDirName: String, config: FakeNodeConfig) { @@ -286,5 +363,10 @@ class NetworkBootstrapperTest { } } + private fun assertContainsPackageOwner(nodeDirName: String, packageOwners: Map) { + val networkParams = (rootDir / nodeDirName).networkParameters + assertThat(networkParams.packageOwnership).isEqualTo(packageOwners) + } + data class FakeNodeConfig(val myLegalName: CordaX500Name, val notary: NotaryConfig? = null) } diff --git a/node/src/test/kotlin/net/corda/node/internal/NetworkParametersTest.kt b/node/src/test/kotlin/net/corda/node/internal/NetworkParametersTest.kt index e5a352d6e7..3544ee5e85 100644 --- a/node/src/test/kotlin/net/corda/node/internal/NetworkParametersTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/NetworkParametersTest.kt @@ -107,7 +107,7 @@ class NetworkParametersTest { JavaPackageName("com.!example.stuff") to key2 ) ) - }.withMessageContaining("Attempting to whitelist illegal java package") + }.withMessageContaining("Invalid Java package name") assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy { NetworkParameters(1, diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt index 9eac667dc1..9e12b9b60e 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt @@ -6,9 +6,6 @@ import com.google.common.jimfs.Configuration import com.google.common.jimfs.Jimfs import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.whenever -import net.corda.core.JarSignatureTestUtils.createJar -import net.corda.core.JarSignatureTestUtils.generateKey -import net.corda.core.JarSignatureTestUtils.signJar import net.corda.core.contracts.ContractAttachment import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 @@ -25,9 +22,11 @@ import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.JarSignatureTestUtils.createJar +import net.corda.testing.core.JarSignatureTestUtils.generateKey +import net.corda.testing.core.JarSignatureTestUtils.signJar import net.corda.testing.internal.LogHelper import net.corda.testing.internal.TestingNamedCacheFactory -import net.corda.testing.internal.rigorousMock import net.corda.testing.internal.configureDatabase import net.corda.testing.internal.rigorousMock import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/ResolveStatePointersTest.kt b/node/src/test/kotlin/net/corda/node/services/transactions/ResolveStatePointersTest.kt new file mode 100644 index 0000000000..847d6daccd --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/services/transactions/ResolveStatePointersTest.kt @@ -0,0 +1,158 @@ +package net.corda.node.services.transactions + +import net.corda.core.contracts.* +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.CordaX500Name +import net.corda.core.transactions.TransactionBuilder +import net.corda.testing.common.internal.testNetworkParameters +import net.corda.testing.contracts.DummyContract +import net.corda.testing.core.DUMMY_NOTARY_NAME +import net.corda.testing.core.SerializationEnvironmentRule +import net.corda.testing.core.TestIdentity +import net.corda.testing.node.MockServices +import net.corda.testing.node.makeTestIdentityService +import org.junit.Rule +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class ResolveStatePointersTest { + + @Rule + @JvmField + val testSerialization = SerializationEnvironmentRule() + + private val myself = TestIdentity(CordaX500Name("Me", "London", "GB")) + private val notary = TestIdentity(DUMMY_NOTARY_NAME, 20) + private val cordapps = listOf("net.corda.testing.contracts") + private val databaseAndServices = MockServices.makeTestDatabaseAndMockServices( + cordappPackages = cordapps, + identityService = makeTestIdentityService(notary.identity, myself.identity), + initialIdentity = myself, + networkParameters = testNetworkParameters(minimumPlatformVersion = 4) + ) + + private val services = databaseAndServices.second + + private data class Bar( + override val participants: List = listOf(), + val bar: Int = 0, + val nestedPointer: LinearPointer<*>? = null, + override val linearId: UniqueIdentifier = UniqueIdentifier() + ) : LinearState + + private data class Foo(val baz: LinearPointer, override val participants: List) : ContractState + + private val barOne = Bar(listOf(myself.party), 1) + private val barTwo = Bar(listOf(myself.party), 2, LinearPointer(barOne.linearId, barOne::class.java)) + + private fun createPointedToState(contractState: ContractState): StateAndRef { + // Create the pointed to state. + return services.run { + val tx = signInitialTransaction(TransactionBuilder(notary = notary.party, serviceHub = services).apply { + addOutputState(contractState, DummyContract.PROGRAM_ID) + addCommand(Command(DummyContract.Commands.Create(), myself.party.owningKey)) + }) + recordTransactions(listOf(tx)) + tx.tx.outRefsOfType().single() + } + } + + @Test + fun `resolve state pointers and check reference state is added to transaction`() { + val stateAndRef = createPointedToState(barOne) + val linearId = stateAndRef.state.data.linearId + + // Add a new state containing a linear pointer. + val tx = TransactionBuilder(notary = notary.party, serviceHub = services).apply { + val pointer = LinearPointer(linearId, barOne::class.java) + addOutputState(Foo(pointer, listOf(myself.party)), DummyContract.PROGRAM_ID) + addCommand(Command(DummyContract.Commands.Create(), myself.party.owningKey)) + } + + // Check the StateRef for the pointed-to state is added as a reference. + assertEquals(stateAndRef.ref, tx.referenceStates().single()) + + // Resolve the StateRef to the actual state. + val ltx = tx.toLedgerTransaction(services) + assertEquals(barOne, ltx.referenceStates.single()) + } + + @Test + fun `resolving nested pointers is possible`() { + // Create barOne. + createPointedToState(barOne) + + // Create another Bar - barTwo - which points to barOne. + val barTwoStateAndRef = createPointedToState(barTwo) + val barTwoLinearId = barTwoStateAndRef.state.data.linearId + + // Add a new state containing a linear pointer. + val tx = TransactionBuilder(notary = notary.party, serviceHub = services).apply { + val pointer = LinearPointer(barTwoLinearId, barTwo::class.java) + addOutputState(Foo(pointer, listOf(myself.party)), DummyContract.PROGRAM_ID) + addOutputState(Foo(pointer, listOf()), DummyContract.PROGRAM_ID) + addCommand(Command(DummyContract.Commands.Create(), myself.party.owningKey)) + } + + tx.toLedgerTransaction(services).referenceStates.forEach { println(it) } + + // Check both Bar StateRefs have been added to the transaction. + assertEquals(2, tx.referenceStates().size) + } + + @Test + fun `Resolving to an unknown state throws an exception`() { + // Don't create the pointed to state. + // Resolve the pointer for barTwo. + assertFailsWith(IllegalStateException::class) { + barTwo.nestedPointer?.resolve(services) + } + } + + @Test + fun `resolving an exited state throws an exception`() { + // Create barOne. + val stateAndRef = createPointedToState(barOne) + + // Exit barOne from the ledger. + services.run { + val tx = signInitialTransaction(TransactionBuilder(notary = notary.party, serviceHub = services).apply { + addInputState(stateAndRef) + addCommand(Command(DummyContract.Commands.Move(), myself.party.owningKey)) + }) + recordTransactions(listOf(tx)) + } + + assertFailsWith(IllegalStateException::class) { + barTwo.nestedPointer?.resolve(services) + } + } + + @Test + fun `resolve linear pointer with correct type`() { + val stateAndRef = createPointedToState(barOne) + val linearPointer = LinearPointer(stateAndRef.state.data.linearId, barOne::class.java) + val resolvedPointer = linearPointer.resolve(services) + assertEquals(stateAndRef::class.java, resolvedPointer::class.java) + } + + @Test + fun `resolve state pointer in ledger transaction`() { + val stateAndRef = createPointedToState(barOne) + val linearId = stateAndRef.state.data.linearId + + // Add a new state containing a linear pointer. + val tx = TransactionBuilder(notary = notary.party, serviceHub = services).apply { + val pointer = LinearPointer(linearId, barOne::class.java) + addOutputState(Foo(pointer, listOf(myself.party)), DummyContract.PROGRAM_ID) + addCommand(Command(DummyContract.Commands.Create(), myself.party.owningKey)) + } + + val ltx = tx.toLedgerTransaction(services) + @Suppress("UNCHECKED_CAST") + val foo = ltx.outputs.single().data as Foo + assertEquals(stateAndRef, foo.baz.resolve(ltx)) + } + +} \ No newline at end of file diff --git a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt index 3f96dbfef6..9f47870d86 100644 --- a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt +++ b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt @@ -50,6 +50,7 @@ class TraderDemoTest : IntegrationTest() { startNode(providedName = DUMMY_BANK_B_NAME, rpcUsers = listOf(demoUser)), startNode(providedName = BOC_NAME, rpcUsers = listOf(bankUser)) ).map { (it.getOrThrow() as InProcess) } + val (nodeARpc, nodeBRpc) = listOf(nodeA, nodeB).map { val client = CordaRPCClient(it.rpcAddress) client.start(demoUser.username, demoUser.password).proxy diff --git a/core/src/test/kotlin/net/corda/core/JarSignatureTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/core/JarSignatureTestUtils.kt similarity index 70% rename from core/src/test/kotlin/net/corda/core/JarSignatureTestUtils.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/core/JarSignatureTestUtils.kt index 273d5b2234..903d05fca7 100644 --- a/core/src/test/kotlin/net/corda/core/JarSignatureTestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/core/JarSignatureTestUtils.kt @@ -1,4 +1,4 @@ -package net.corda.core +package net.corda.testing.core import net.corda.core.internal.JarSignatureCollector import net.corda.core.internal.div @@ -25,8 +25,8 @@ object JarSignatureTestUtils { .waitFor()) } - fun Path.generateKey(alias: String, password: String, name: String, keyalg: String = "RSA") = - executeProcess("keytool", "-genkey", "-keystore", "_teststore", "-storepass", "storepass", "-keyalg", keyalg, "-alias", alias, "-keypass", password, "-dname", name) + fun Path.generateKey(alias: String, storePassword: String, name: String, keyalg: String = "RSA", keyPassword: String = storePassword, storeName: String = "_teststore") = + executeProcess("keytool", "-genkeypair", "-keystore" ,storeName, "-storepass", storePassword, "-keyalg", keyalg, "-alias", alias, "-keypass", keyPassword, "-dname", name) fun Path.createJar(fileName: String, vararg contents: String) = executeProcess(*(arrayOf("jar", "cvf", fileName) + contents)) @@ -34,9 +34,9 @@ object JarSignatureTestUtils { fun Path.updateJar(fileName: String, vararg contents: String) = executeProcess(*(arrayOf("jar", "uvf", fileName) + contents)) - fun Path.signJar(fileName: String, alias: String, password: String): PublicKey { - executeProcess("jarsigner", "-keystore", "_teststore", "-storepass", "storepass", "-keypass", password, fileName, alias) - val ks = loadKeyStore(this.resolve("_teststore"), "storepass") + fun Path.signJar(fileName: String, alias: String, storePassword: String, keyPassword: String = storePassword): PublicKey { + executeProcess("jarsigner", "-keystore", "_teststore", "-storepass", storePassword, "-keypass", keyPassword, fileName, alias) + val ks = loadKeyStore(this.resolve("_teststore"), storePassword) return ks.getCertificate(alias).publicKey } diff --git a/core/src/test/kotlin/net/corda/core/internal/JarSignatureCollectorTest.kt b/testing/test-utils/src/test/kotlin/net/corda/testing/core/JarSignatureCollectorTest.kt similarity index 84% rename from core/src/test/kotlin/net/corda/core/internal/JarSignatureCollectorTest.kt rename to testing/test-utils/src/test/kotlin/net/corda/testing/core/JarSignatureCollectorTest.kt index 4c124e81b7..3f356d5e7f 100644 --- a/core/src/test/kotlin/net/corda/core/internal/JarSignatureCollectorTest.kt +++ b/testing/test-utils/src/test/kotlin/net/corda/testing/core/JarSignatureCollectorTest.kt @@ -1,14 +1,14 @@ -package net.corda.core.internal +package net.corda.testing.core -import net.corda.core.JarSignatureTestUtils.createJar -import net.corda.core.JarSignatureTestUtils.generateKey -import net.corda.core.JarSignatureTestUtils.getJarSigners -import net.corda.core.JarSignatureTestUtils.signJar -import net.corda.core.JarSignatureTestUtils.updateJar +import net.corda.testing.core.JarSignatureTestUtils.createJar +import net.corda.testing.core.JarSignatureTestUtils.generateKey +import net.corda.testing.core.JarSignatureTestUtils.getJarSigners +import net.corda.testing.core.JarSignatureTestUtils.signJar +import net.corda.testing.core.JarSignatureTestUtils.updateJar import net.corda.core.identity.Party +import net.corda.core.internal.* import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME -import net.corda.testing.core.CHARLIE_NAME import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.AfterClass @@ -34,8 +34,8 @@ class JarSignatureCollectorTest { @BeforeClass @JvmStatic fun beforeClass() { - dir.generateKey(ALICE, ALICE_PASS, ALICE_NAME.toString()) - dir.generateKey(BOB, BOB_PASS, BOB_NAME.toString()) + dir.generateKey(ALICE, "storepass", ALICE_NAME.toString(), keyPassword = ALICE_PASS) + dir.generateKey(BOB, "storepass", BOB_NAME.toString(), keyPassword = BOB_PASS) (dir / "_signable1").writeLines(listOf("signable1")) (dir / "_signable2").writeLines(listOf("signable2")) @@ -134,12 +134,12 @@ class JarSignatureCollectorTest { // and our JarSignatureCollector @Test fun `one signer with EC algorithm`() { - dir.generateKey(CHARLIE, CHARLIE_PASS, CHARLIE_NAME.toString(), "EC") + dir.generateKey(CHARLIE, "storepass", CHARLIE_NAME.toString(), "EC", CHARLIE_PASS) dir.createJar(FILENAME, "_signable1", "_signable2") - val key = dir.signJar(FILENAME, CHARLIE, CHARLIE_PASS) + val key = dir.signJar(FILENAME, CHARLIE, "storepass", CHARLIE_PASS) assertEquals(listOf(key), dir.getJarSigners(FILENAME)) // We only used CHARLIE's distinguished name, so the keys will be different. } - private fun signAsAlice() = dir.signJar(FILENAME, ALICE, ALICE_PASS) - private fun signAsBob() = dir.signJar(FILENAME, BOB, BOB_PASS) + private fun signAsAlice() = dir.signJar(FILENAME, ALICE, "storepass", ALICE_PASS) + private fun signAsBob() = dir.signJar(FILENAME, BOB, "storepass", BOB_PASS) } diff --git a/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt b/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt index 3436bdfaf3..ed21e8cb26 100644 --- a/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt +++ b/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt @@ -3,10 +3,16 @@ package net.corda.bootstrapper import net.corda.cliutils.CordaCliWrapper import net.corda.cliutils.start import net.corda.core.internal.PLATFORM_VERSION +import net.corda.core.node.JavaPackageName +import net.corda.nodeapi.internal.crypto.loadKeyStore import net.corda.nodeapi.internal.network.NetworkBootstrapper +import picocli.CommandLine import picocli.CommandLine.Option +import java.io.IOException import java.nio.file.Path import java.nio.file.Paths +import java.security.KeyStoreException +import java.security.PublicKey fun main(args: Array) { NetworkBootstrapperRunner().start(args) @@ -20,16 +26,91 @@ class NetworkBootstrapperRunner : CordaCliWrapper("bootstrapper", "Bootstrap a l "It may also contain existing node directories." ] ) - private var dir: Path = Paths.get(".") + var dir: Path = Paths.get(".") @Option(names = ["--no-copy"], description = ["""Don't copy the CorDapp JARs into the nodes' "cordapps" directories."""]) - private var noCopy: Boolean = false + var noCopy: Boolean = false @Option(names = ["--minimum-platform-version"], description = ["The minimumPlatformVersion to use in the network-parameters."]) - private var minimumPlatformVersion = PLATFORM_VERSION + var minimumPlatformVersion = PLATFORM_VERSION + + @Option(names = ["--register-package-owner"], + converter = [PackageOwnerConverter::class], + description = [ + "Register owner of Java package namespace in the network-parameters.", + "Format: [java-package-namespace;keystore-file;password;alias]", + " `java-package-namespace` is case insensitive and cannot be a sub-package of an existing registered namespace", + " `keystore-file` refers to the location of key store file containing the signed certificate as generated by the Java 'keytool' tool (see https://docs.oracle.com/javase/8/docs/technotes/tools/windows/keytool.html)", + " `password` to open the key store", + " `alias` refers to the name associated with a certificate containing the public key to be associated with the package namespace" + ]) + var registerPackageOwnership: List = mutableListOf() + + @Option(names = ["--unregister-package-owner"], + converter = [JavaPackageNameConverter::class], + description = [ + "Unregister owner of Java package namespace in the network-parameters.", + "Format: [java-package-namespace]", + " `java-package-namespace` is case insensitive and cannot be a sub-package of an existing registered namespace" + ]) + var unregisterPackageOwnership: List = mutableListOf() override fun runProgram(): Int { - NetworkBootstrapper().bootstrap(dir.toAbsolutePath().normalize(), copyCordapps = !noCopy, minimumPlatformVersion = minimumPlatformVersion) + NetworkBootstrapper().bootstrap(dir.toAbsolutePath().normalize(), + copyCordapps = !noCopy, + minimumPlatformVersion = minimumPlatformVersion, + packageOwnership = registerPackageOwnership.map { Pair(it.javaPackageName, it.publicKey) }.toMap() + .plus(unregisterPackageOwnership.map { Pair(it, null) }) + ) return 0 //exit code } -} \ No newline at end of file +} + + +data class PackageOwner(val javaPackageName: JavaPackageName, val publicKey: PublicKey) + +/** + * Converter from String to PackageOwner (JavaPackageName and PublicKey) + */ +class PackageOwnerConverter : CommandLine.ITypeConverter { + override fun convert(packageOwner: String): PackageOwner { + if (!packageOwner.isBlank()) { + val packageOwnerSpec = packageOwner.split(";") + if (packageOwnerSpec.size < 4) + throw IllegalArgumentException("Package owner must specify 4 elements separated by semi-colon: 'java-package-namespace;keyStorePath;keyStorePassword;alias'") + // java package name validation + val javaPackageName = JavaPackageName(packageOwnerSpec[0]) + // cater for passwords that include the argument delimiter field + val keyStorePassword = + if (packageOwnerSpec.size > 4) + packageOwnerSpec.subList(2, packageOwnerSpec.size-1).joinToString(";") + else packageOwnerSpec[2] + try { + val ks = loadKeyStore(Paths.get(packageOwnerSpec[1]), keyStorePassword) + try { + val publicKey = ks.getCertificate(packageOwnerSpec[packageOwnerSpec.size-1]).publicKey + return PackageOwner(javaPackageName,publicKey) + } + catch(kse: KeyStoreException) { + throw IllegalArgumentException("Keystore has not been initialized for alias ${packageOwnerSpec[3]}") + } + } + catch(kse: KeyStoreException) { + throw IllegalArgumentException("Password is incorrect or the key store is damaged for keyStoreFilePath: ${packageOwnerSpec[1]} and keyStorePassword: $keyStorePassword") + } + catch(e: IOException) { + throw IllegalArgumentException("Error reading the key store from the file for keyStoreFilePath: ${packageOwnerSpec[1]} and keyStorePassword: $keyStorePassword") + } + } + else throw IllegalArgumentException("Must specify package owner argument: 'java-package-namespace;keyStorePath;keyStorePassword;alias'") + } +} + +/** + * Converter from String to JavaPackageName. + */ +class JavaPackageNameConverter : CommandLine.ITypeConverter { + override fun convert(packageName: String): JavaPackageName { + return JavaPackageName(packageName) + } +} diff --git a/tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/PackageOwnerParsingTest.kt b/tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/PackageOwnerParsingTest.kt new file mode 100644 index 0000000000..3caf0285d3 --- /dev/null +++ b/tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/PackageOwnerParsingTest.kt @@ -0,0 +1,167 @@ +package net.corda.bootstrapper + +import net.corda.core.internal.deleteRecursively +import net.corda.core.internal.div +import net.corda.core.node.JavaPackageName +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.BOB_NAME +import net.corda.testing.core.CHARLIE_NAME +import net.corda.testing.core.JarSignatureTestUtils.generateKey +import org.assertj.core.api.Assertions.assertThat +import org.junit.AfterClass +import org.junit.BeforeClass +import org.junit.Rule +import org.junit.Test +import org.junit.rules.ExpectedException +import picocli.CommandLine +import java.nio.file.Files + +class PackageOwnerParsingTest { + + @Rule + @JvmField + val expectedEx: ExpectedException = ExpectedException.none() + + companion object { + + private const val ALICE = "alice" + private const val ALICE_PASS = "alicepass" + private const val BOB = "bob" + private const val BOB_PASS = "bobpass" + private const val CHARLIE = "charlie" + private const val CHARLIE_PASS = "charliepass" + + private val dirAlice = Files.createTempDirectory(ALICE) + private val dirBob = Files.createTempDirectory(BOB) + private val dirCharlie = Files.createTempDirectory(CHARLIE) + + val networkBootstrapper = NetworkBootstrapperRunner() + val commandLine = CommandLine(networkBootstrapper) + + @BeforeClass + @JvmStatic + fun beforeClass() { + dirAlice.generateKey(ALICE, ALICE_PASS, ALICE_NAME.toString()) + dirBob.generateKey(BOB, BOB_PASS, BOB_NAME.toString(), "EC") + dirCharlie.generateKey(CHARLIE, CHARLIE_PASS, CHARLIE_NAME.toString(), "DSA") + } + + @AfterClass + @JvmStatic + fun afterClass() { + dirAlice.deleteRecursively() + } + } + + @Test + fun `parse registration request with single mapping`() { + val aliceKeyStorePath = dirAlice / "_teststore" + val args = arrayOf("--register-package-owner", "com.example.stuff;$aliceKeyStorePath;$ALICE_PASS;$ALICE") + commandLine.parse(*args) + assertThat(networkBootstrapper.registerPackageOwnership[0].javaPackageName).isEqualTo(JavaPackageName("com.example.stuff")) + } + + @Test + fun `parse registration request with invalid arguments`() { + val args = arrayOf("--register-package-owner", "com.!example.stuff") + expectedEx.expect(CommandLine.ParameterException::class.java) + expectedEx.expectMessage("Package owner must specify 4 elements separated by semi-colon") + commandLine.parse(*args) + } + + @Test + fun `parse registration request with incorrect keystore specification`() { + val aliceKeyStorePath = dirAlice / "_teststore" + val args = arrayOf("--register-package-owner", "com.example.stuff;$aliceKeyStorePath$ALICE_PASS") + expectedEx.expect(CommandLine.ParameterException::class.java) + expectedEx.expectMessage("Package owner must specify 4 elements separated by semi-colon") + commandLine.parse(*args) + } + + @Test + fun `parse registration request with invalid java package name`() { + val args = arrayOf("--register-package-owner", "com.!example.stuff;A;B;C") + expectedEx.expect(CommandLine.ParameterException::class.java) + expectedEx.expectMessage("Invalid Java package name") + commandLine.parse(*args) + } + + @Test + fun `parse registration request with invalid keystore file`() { + val args = arrayOf("--register-package-owner", "com.example.stuff;NONSENSE;B;C") + expectedEx.expect(CommandLine.ParameterException::class.java) + expectedEx.expectMessage("Error reading the key store from the file") + commandLine.parse(*args) + } + + @Test + fun `parse registration request with invalid keystore password`() { + val aliceKeyStorePath = dirAlice / "_teststore" + val args = arrayOf("--register-package-owner", "com.example.stuff;$aliceKeyStorePath;BAD_PASSWORD;$ALICE") + expectedEx.expect(CommandLine.ParameterException::class.java) + expectedEx.expectMessage("Error reading the key store from the file") + commandLine.parse(*args) + } + + @Test + fun `parse registration request with invalid keystore alias`() { + val aliceKeyStorePath = dirAlice / "_teststore" + val args = arrayOf("--register-package-owner", "com.example.stuff;$aliceKeyStorePath;$ALICE_PASS;BAD_ALIAS") + expectedEx.expect(CommandLine.ParameterException::class.java) + expectedEx.expectMessage("must not be null") + commandLine.parse(*args) + } + + @Test + fun `parse registration request with multiple arguments`() { + val aliceKeyStorePath = dirAlice / "_teststore" + val bobKeyStorePath = dirBob / "_teststore" + val charlieKeyStorePath = dirCharlie / "_teststore" + val args = arrayOf("--register-package-owner", "com.example.stuff;$aliceKeyStorePath;$ALICE_PASS;$ALICE", + "--register-package-owner", "com.example.more.stuff;$bobKeyStorePath;$BOB_PASS;$BOB", + "--register-package-owner", "com.example.even.more.stuff;$charlieKeyStorePath;$CHARLIE_PASS;$CHARLIE") + commandLine.parse(*args) + assertThat(networkBootstrapper.registerPackageOwnership).hasSize(3) + } + + @Test + fun `parse registration request with delimiter inclusive passwords`() { + val aliceKeyStorePath1 = dirAlice / "_alicestore1" + dirAlice.generateKey("${ALICE}1", "passw;rd", ALICE_NAME.toString(), storeName = "_alicestore1") + val aliceKeyStorePath2 = dirAlice / "_alicestore2" + dirAlice.generateKey("${ALICE}2", "\"passw;rd\"", ALICE_NAME.toString(), storeName = "_alicestore2") + val aliceKeyStorePath3 = dirAlice / "_alicestore3" + dirAlice.generateKey("${ALICE}3", "passw;rd", ALICE_NAME.toString(), storeName = "_alicestore3") + val aliceKeyStorePath4 = dirAlice / "_alicestore4" + dirAlice.generateKey("${ALICE}4", "\'passw;rd\'", ALICE_NAME.toString(), storeName = "_alicestore4") + val aliceKeyStorePath5 = dirAlice / "_alicestore5" + dirAlice.generateKey("${ALICE}5", "\"\"passw;rd\"\"", ALICE_NAME.toString(), storeName = "_alicestore5") + val packageOwnerSpecs = listOf("net.something0;$aliceKeyStorePath1;passw;rd;${ALICE}1", + "net.something1;$aliceKeyStorePath2;\"passw;rd\";${ALICE}2", + "\"net.something2;$aliceKeyStorePath3;passw;rd;${ALICE}3\"", + "net.something3;$aliceKeyStorePath4;\'passw;rd\';${ALICE}4", + "net.something4;$aliceKeyStorePath5;\"\"passw;rd\"\";${ALICE}5") + packageOwnerSpecs.forEachIndexed { i, packageOwnerSpec -> + commandLine.parse(*arrayOf("--register-package-owner", packageOwnerSpec)) + assertThat(networkBootstrapper.registerPackageOwnership[0].javaPackageName).isEqualTo(JavaPackageName("net.something$i")) + } + } + + @Test + fun `parse unregister request with single mapping`() { + val args = arrayOf("--unregister-package-owner", "com.example.stuff") + commandLine.parse(*args) + assertThat(networkBootstrapper.unregisterPackageOwnership).contains(JavaPackageName("com.example.stuff")) + } + + @Test + fun `parse mixed register and unregister request`() { + val aliceKeyStorePath = dirAlice / "_teststore" + val args = arrayOf("--register-package-owner", "com.example.stuff;$aliceKeyStorePath;$ALICE_PASS;$ALICE", + "--unregister-package-owner", "com.example.stuff2") + commandLine.parse(*args) + assertThat(networkBootstrapper.registerPackageOwnership.map { it.javaPackageName }).contains(JavaPackageName("com.example.stuff")) + assertThat(networkBootstrapper.unregisterPackageOwnership).contains(JavaPackageName("com.example.stuff2")) + } +} +