Merge pull request #1543 from corda/colljos-merge-to-015a36dad

OS -> ENT merge
This commit is contained in:
josecoll 2018-11-06 22:16:13 +00:00 committed by GitHub
commit b692c59553
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1409 additions and 139 deletions

View File

@ -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 <init>(net.corda.core.contracts.UniqueIdentifier, Class<T>)
@NotNull
public net.corda.core.contracts.UniqueIdentifier getPointer()
@NotNull
public Class<T> getType()
@NotNull
public net.corda.core.contracts.StateAndRef<T> resolve(net.corda.core.node.ServiceHub)
@NotNull
public net.corda.core.contracts.StateAndRef<T> 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<T> getType()
@NotNull
public abstract net.corda.core.contracts.StateAndRef<T> resolve(net.corda.core.node.ServiceHub)
@NotNull
public abstract net.corda.core.contracts.StateAndRef<T> resolve(net.corda.core.transactions.LedgerTransaction)
##
@CordaSerializable
public final class net.corda.core.contracts.StateRef extends java.lang.Object
public <init>(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 <init>(net.corda.core.contracts.StateRef, Class<T>)
@NotNull
public net.corda.core.contracts.StateRef getPointer()
@NotNull
public Class<T> getType()
@NotNull
public net.corda.core.contracts.StateAndRef<T> resolve(net.corda.core.node.ServiceHub)
@NotNull
public net.corda.core.contracts.StateAndRef<T> 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)

View File

@ -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<T : ContractState> {
/**
* 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<T>
/**
* 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<T>
/**
* 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<T>
}
/**
* 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<T : ContractState>(override val pointer: StateRef, override val type: Class<T>) : StatePointer<T>() {
/**
* Resolves a [StaticPointer] to a [StateAndRef] via a [StateRef] look-up.
*/
@Throws(TransactionResolutionException::class)
@Suppress("UNCHECKED_CAST")
override fun resolve(services: ServiceHub): StateAndRef<T> {
val transactionState = services.loadState(pointer) as TransactionState<T>
val castState: T = type.cast(transactionState.data)
val castTransactionState: TransactionState<T> = transactionState.copy(data = castState)
return StateAndRef(castTransactionState, pointer)
}
override fun resolve(ltx: LedgerTransaction): StateAndRef<T> {
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<T : LinearState>(override val pointer: UniqueIdentifier, override val type: Class<T>) : StatePointer<T>() {
/**
* 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<T> {
// 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<StateAndRef<LinearState>> = services.vaultService.queryBy<LinearState>(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<T>
return StateAndRef(castTransactionState, stateAndRef.ref)
}
override fun resolve(ltx: LedgerTransaction): StateAndRef<T> {
return ltx.referenceInputRefsOfType(type).single { pointer == it.state.data.linearId }
}
}

View File

@ -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<StatePointer<*>>()
// Record seen objects to avoid getting stuck in loops.
private val seenObjects = mutableSetOf<Any>().apply { add(state) }
// Queue of fields to search.
private val fieldQueue = ArrayDeque<FieldWithObject>().apply { addAllFields(state) }
// Helper for adding all fields to the queue.
private fun ArrayDeque<FieldWithObject>.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<StatePointer<*>> {
while (fieldQueue.isNotEmpty()) {
val (obj, field) = fieldQueue.pop()
field.isAccessible = true
handleField(obj, field)
}
return statePointers
}
}

View File

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

View File

@ -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<Command<*>> = arrayListOf(),
protected var window: TimeWindow? = null,
protected var privacySalt: PrivacySalt = PrivacySalt(),
protected val references: MutableList<StateRef> = arrayListOf()
protected val references: MutableList<StateRef> = 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<StatePointer<*>>().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. */

View File

@ -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 <K, V> Map<K, V?>.filterNotNullValues() = filterValues { it != null } as Map<K, V>
/**
* Usually you won't need this method:
* * If you're in a companion object, use [contextLogger]

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
Supported cipher suites
=======================
Cipher suites supported by Corda
================================
.. contents::

View File

@ -4,54 +4,29 @@
<script type="text/javascript" src="_static/jquery.js"></script>
<script type="text/javascript" src="_static/codesets.js"></script>
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 <trust store 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`.

View File

@ -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 R3s 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 R3s 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 Operators 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
Networks 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 Foundations 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.

View File

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

View File

@ -1,5 +1,5 @@
The Corda Testnet
=================
Joining Corda Testnet
=====================
.. contents::

View File

View File

@ -0,0 +1,10 @@
Deploying Corda to Testnet
==========================
.. toctree::
:maxdepth: 1
azure-vm-explore
aws-vm-explore
gcp-vm
deploy-locally

View File

@ -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<T>` as a property inside another `ContractState`:
```kotlin
// StateRef.
data class FooState(val ref: StateRef) : ContractState
// StateAndRef.
data class FooState(val ref: StateAndRef<BarState>) : ContractState
```
Linking to a `StateRef` or `StateAndRef<T>` 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<ContractState>
}
```
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<ContractState> {
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<T>` 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<LinearState> {
val query = QueryCriteria.LinearStateQueryCriteria(linearId = listOf(pointer))
val result = services.vaultService.queryBy<LinearState>(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<T>` 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".

View File

@ -0,0 +1,58 @@
.. highlight:: kotlin
.. raw:: html
<script type="text/javascript" src="_static/jquery.js"></script>
<script type="text/javascript" src="_static/codesets.js"></script>
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 <trust store 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.

View File

@ -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<design/data-model-upgrades/package-namespace-ownership>`.
A Java package namespace is case insensitive and cannot be a sub-package of an existing registered namespace.
See `Naming a Package <https://docs.oracle.com/javase/tutorial/java/package/namingpkgs.html>`_ and `Naming Conventions <https://www.oracle.com/technetwork/java/javase/documentation/codeconventions-135099.html#28840 for guidelines and conventions>`_ for guidelines on naming conventions.
Registration of a java package namespace requires creation of a signed certificate as generated by the
`Java keytool <https://docs.oracle.com/javase/8/docs/technotes/tools/windows/keytool.html>`_.
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 <https://github.com/corda/cordapp-example>`_ 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 <https://docs.corda.net/tutorial-cordapp.html#building-the-example-cordapp>`_.
.. 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=<dir>] [--logging-level=<loggingLevel>]
[--minimum-platform-version=<minimumPlatformVersion>] [COMMAND]
[--minimum-platform-version=<minimumPlatformVersion>]
[--register-package-owner java-package-namespace=keystore-file:password:alias]
[--unregister-package-owner java-package-namespace]
[COMMAND]
* ``--dir=<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.
``install-shell-extensions``: Install ``bootstrapper`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info.

View File

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

View File

@ -4,8 +4,8 @@
<script type="text/javascript" src="_static/jquery.js"></script>
<script type="text/javascript" src="_static/codesets.js"></script>
Network permissioning
=====================
Network certificates
====================
.. contents::

View File

@ -0,0 +1,191 @@
.. highlight:: kotlin
.. raw:: html
<script type="text/javascript" src="_static/jquery.js"></script>
<script type="text/javascript" src="_static/codesets.js"></script>
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 <https://medium.com/corda/cordas-new-network-map-infrastructure-8c4c248fd7f3>`__.
**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 <https://github.com/corda/corda-solutions/tree/master/bn-apps/memberships-management>`_.
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<Party>`` 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 <https://gitlab.com/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<NetworkParameters>``
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<SignedDataWithCert<NetworkParameters>> 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<SignedNetworkParameters> = signingCertAndKeyPair.sign(networkParameters).serialize()
signedParams.open().copyTo(Paths.get("/some/path"))
Each individual parameter is documented in `the JavaDocs/KDocs for the NetworkParameters class
<https://docs.corda.net/api/kotlin/corda/net.corda.core.node/-network-parameters/index.html>`__. 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 <https://groups.io/g/corda-dev>`__.

View File

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

View File

@ -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<JavaPackageName, PublicKey?> = 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<Path>,
copyCordapps: Boolean,
fromCordform: Boolean,
minimumPlatformVersion: Int = PLATFORM_VERSION
minimumPlatformVersion: Int = PLATFORM_VERSION,
packageOwnership : Map<JavaPackageName, PublicKey?> = 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<String, List<AttachmentId>>,
existingNetParams: NetworkParameters?,
nodeDirs: List<Path>,
minimumPlatformVersion: Int
minimumPlatformVersion: Int,
packageOwnership : Map<JavaPackageName, PublicKey?>
): 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
)

View File

@ -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<JavaPackageName, PublicKey?> = 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<JavaPackageName, PublicKey>) {
val networkParams = (rootDir / nodeDirName).networkParameters
assertThat(networkParams.packageOwnership).isEqualTo(packageOwners)
}
data class FakeNodeConfig(val myLegalName: CordaX500Name, val notary: NotaryConfig? = null)
}

View File

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

View File

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

View File

@ -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<AbstractParty> = listOf(),
val bar: Int = 0,
val nestedPointer: LinearPointer<*>? = null,
override val linearId: UniqueIdentifier = UniqueIdentifier()
) : LinearState
private data class Foo<T : LinearState>(val baz: LinearPointer<T>, override val participants: List<AbstractParty>) : 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<Bar> {
// 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<Bar>().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<Bar>
assertEquals(stateAndRef, foo.baz.resolve(ltx))
}
}

View File

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

View File

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

View File

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

View File

@ -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<String>) {
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<PackageOwner> = 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<JavaPackageName> = 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
}
}
}
data class PackageOwner(val javaPackageName: JavaPackageName, val publicKey: PublicKey)
/**
* Converter from String to PackageOwner (JavaPackageName and PublicKey)
*/
class PackageOwnerConverter : CommandLine.ITypeConverter<PackageOwner> {
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<JavaPackageName> {
override fun convert(packageName: String): JavaPackageName {
return JavaPackageName(packageName)
}
}

View File

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