mirror of
https://github.com/corda/corda.git
synced 2025-03-15 08:41:04 +00:00
Merge pull request #1543 from corda/colljos-merge-to-015a36dad
OS -> ENT merge
This commit is contained in:
commit
b692c59553
@ -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)
|
||||
|
121
core/src/main/kotlin/net/corda/core/contracts/StatePointer.kt
Normal file
121
core/src/main/kotlin/net/corda/core/contracts/StatePointer.kt
Normal 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 }
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
@ -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. */
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
@ -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.
|
@ -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
|
||||
|
@ -1,5 +1,5 @@
|
||||
Supported cipher suites
|
||||
=======================
|
||||
Cipher suites supported by Corda
|
||||
================================
|
||||
|
||||
.. contents::
|
||||
|
||||
|
@ -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`.
|
||||
|
@ -1,5 +1,5 @@
|
||||
Corda Network Foundation : Governance Guidelines
|
||||
====================================================
|
||||
================================================
|
||||
|
||||
23 October 2018
|
||||
|
||||
@ -11,7 +11,7 @@ This is a set of governance guidelines for the Corda Network Foundation. It prov
|
||||
Board, to steer and govern Corda Network effectively to realise its potential. It is not a set of binding legal obligations.
|
||||
|
||||
1 Background to Corda and the Network
|
||||
=====================================
|
||||
-------------------------------------
|
||||
|
||||
Corda allows multiple independent applications and private networks to coexist, each with their own business models and
|
||||
membership criteria, yet linked by the same underlying network (‘Corda Network’). This Network enables ‘interoperability’,
|
||||
@ -23,7 +23,7 @@ specified in the Corda Open Source Project codebase, but later may be formalised
|
||||
which then will become canonical.
|
||||
|
||||
1.1 Reason for a Corda Network Foundation
|
||||
-----------------------------------------
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
R3 has set up and governs by default Corda Network currently (along with Corda). This includes making key decisions
|
||||
around establishing, maintaining and updating standards, policies, and procedures for participation in, and use of,
|
||||
Corda Network.
|
||||
@ -41,10 +41,10 @@ In other words, to achieve the community's objective of Corda ubiquity, it is n
|
||||
structure which explicitly limits R3’s control of Corda Network, and enables this ubiquity.
|
||||
|
||||
2 The Corda Network Foundation
|
||||
==============================
|
||||
------------------------------
|
||||
|
||||
2.1 Mission and Values
|
||||
----------------------
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
Following on from the Corda introduction and technical white papers, we see the mission of the Corda Network Foundation
|
||||
to achieve the vision of Corda - whereby the state of transactions and agreements of business partners can be recorded
|
||||
in a single global database, ending the need for costly reconciliation and error correction, while maintaining privacy.
|
||||
@ -85,7 +85,7 @@ developers, service and solution providers and end users.
|
||||
customers, open source developers, and R3’s shareholders.
|
||||
|
||||
2.2 Structure of the Foundation
|
||||
-------------------------------
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
The Foundation shall be a not-for-profit entity created exclusively to execute the mission set out in this Constitution.
|
||||
With the advice of international lawyers, this is a ‘Stichting’ domiciled in Holland – a legal entity suited for
|
||||
governance activities, able to act commercially, with limited liability but no shareholders, capital or dividends.
|
||||
@ -112,10 +112,10 @@ and with privileges and responsibilities as set out in section 6.
|
||||
Any change to the structure of the Foundation is a constitutional change, described in section 5.1.
|
||||
|
||||
3 Governing Board
|
||||
=================
|
||||
-----------------
|
||||
|
||||
3.1 Role of the Board
|
||||
---------------------
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
The goal of the Board is primarily to ensure the stable and secure operation of the Network, as well as to achieve the
|
||||
vision of Corda laid out in section 2.1. The fundamental responsibility of directors appointed to the Board is to
|
||||
exercise their business judgement to act in what they believe to be the best interests of the Network, taking account
|
||||
@ -129,7 +129,7 @@ The Board is the formal decision-making authority of the Foundation, and actions
|
||||
decision making.
|
||||
|
||||
3.2 Relationship of the Board with the Operator
|
||||
-----------------------------------------------
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
It is the duty of the Board to monitor the Operator’s performance to ensure that the Network operates in an effective,
|
||||
efficient and ethical manner. The Board will also be responsible for overseeing the Operator in the development of the
|
||||
Network’s strategic and tactical plans, ensuring that they will result in broad and open adoption of Corda. The Operator
|
||||
@ -137,7 +137,7 @@ is responsible to the Board for the execution of day to day operations, and the
|
||||
change.
|
||||
|
||||
3.3 Composition and Establishment of the Board
|
||||
----------------------------------------------
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
### 3.3.1 Size
|
||||
The Board shall consist of 11 voting members (‘Directors’) in total, to allow broad representation but maintain an agile
|
||||
decision-making ability. The selection process (using the Participant Community) is intended to ensure that the Board is
|
||||
@ -212,7 +212,7 @@ board meetings of more than six months, death, or if necessary, removal by a Man
|
||||
vacant seat will be contested at the next annual election.
|
||||
|
||||
3.4 Conduct of Board Meetings
|
||||
-----------------------------
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Attendance may be in person or by video-conference.
|
||||
|
||||
The Board shall meet not less than every six months, and may meet on the request of any Director, but not more than every
|
||||
@ -236,7 +236,7 @@ observer per unrepresented Participant. Observers may participate in discussions
|
||||
vote, and may be asked to join by video-conference if there are logistical constraints.
|
||||
|
||||
4 Relation of the Foundation to Business Networks
|
||||
===================================================
|
||||
---------------------------------------------------
|
||||
|
||||
The global Network shall support the operation of any business networks which may be formed by industry-specific
|
||||
operators on top of the Network. The Board shall ensure that there is a clear separation between areas of governance
|
||||
@ -247,14 +247,14 @@ under a Creative Commons license, both for reuse by business network operators i
|
||||
governance structure, and so that such governance layers are complementary and not contradictory.
|
||||
|
||||
5 Governance Events
|
||||
=====================
|
||||
---------------------
|
||||
|
||||
All formal changes to the Network and the Foundation shall be controlled through a formal Governance Event process, and
|
||||
the right to initiate this shall be held by all Directors and Participants. In the event of disruptive behaviour by an
|
||||
individual Participant or group of Participants, this right may be curtailed, as described in 5.2.5.
|
||||
|
||||
5.1 Types of Governance Events
|
||||
------------------------------
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
There are three types of change that affect the Network and the Foundation, which require a controlled change process
|
||||
and a vote described in 5.5, and are defined as Governance Events:
|
||||
|
||||
@ -280,7 +280,7 @@ Advisory Committee to provide due diligence and make a recommendation for implem
|
||||
For all Governance Events, decisions and the rationale for the decision shall be published transparently.
|
||||
|
||||
5.2 Mandatory Governance Events
|
||||
-------------------------------
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
### 5.2.1 Access Standards
|
||||
The Corda system can be accessed by using software which implements the set of technical protocols which define
|
||||
compatibility (see 5.3.1) above). The reference implementation of this software is open source and freely accessible at
|
||||
@ -347,7 +347,7 @@ Change to the arbitration and dispute resolution process shall be the subject of
|
||||
Policies covering areas of operation not covered by the Constitution (e.g. code of conduct for Board Directors).
|
||||
|
||||
5.3 Advisory Governance Events
|
||||
------------------------------
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
#### 5.3.1 Technical Standards
|
||||
There is a set of technical standards, such as ‘network parameters’, which all Corda Network nodes need to comply with
|
||||
in order to guarantee technical compatibility to other nodes and services within the Network. While Corda has stability
|
||||
@ -384,7 +384,7 @@ business network operator, or directly if no business network is involved. If ne
|
||||
the Board by creating an Advisory Governance Event.
|
||||
|
||||
5.4 Emergency Governance Events
|
||||
-------------------------------
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Certain Network incidents, which could impact one or more Network participants and that would be the subject of
|
||||
Mandatory or Advisory Governance Events, shall require immediate resolution. In these cases, the Operator may make
|
||||
emergency changes, but these shall be subject to post-event evaluation and standard Governance Event processing. Areas
|
||||
@ -392,7 +392,7 @@ of control that are the subject of Mandatory Governance Events are not expected
|
||||
the Operator shall be entitled to make emergency changes to preserve the stability and integrity of the Network.
|
||||
|
||||
5.5 Voting
|
||||
----------
|
||||
^^^^^^^^^^
|
||||
All Constitutional, Mandatory and Advisory Governance Events outlined in sections 5.2 and 5.3 shall be presented to the
|
||||
Board for voting. The representatives of the Board shall vote on a one vote per Director basis to approve or reject the
|
||||
Governance Event.
|
||||
@ -416,10 +416,10 @@ in an attempt to simplify governance, provide transparency and lower costs, prov
|
||||
thoroughly and has sufficient manual override controls.
|
||||
|
||||
6 Participation
|
||||
=================
|
||||
-----------------
|
||||
|
||||
6.1 General Membership
|
||||
----------------------
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
Participation is open to any potential participant on the Network, subject to meeting normal Network access conditions
|
||||
described in section 5.2.1, and paying a nominal annual participation fee to cover both the operational costs of Network
|
||||
services and the Foundation, and to ensure that its activities are sufficiently resourced.
|
||||
@ -437,7 +437,7 @@ costs will depend on the individual event.
|
||||
8. Use the Network for live business activities running 'in production'.
|
||||
|
||||
6.2 Technical Advisory Committee
|
||||
--------------------------------
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
The Technical Advisory Committee shall have limited participants appointed directly by the Board. Its mandate and
|
||||
charter will be set by the Board. It shall act directly on the instructions of the Board or the Operator, which shall
|
||||
set expected deliverables and timelines. It shall focus on specific technical topics and may have responsibility for
|
||||
@ -452,7 +452,7 @@ the operation of the Network.
|
||||
elegant and practical system design
|
||||
|
||||
6.3 Governance Advisory Committee
|
||||
---------------------------------
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
The Governance Advisory Committee shall have limited participants appointed directly by the board. Its purpose is to
|
||||
recommend actions to the Board for approval on non-technical matters, where additional support is helpful. This may
|
||||
include decisions on:
|
||||
@ -464,7 +464,7 @@ include decisions on:
|
||||
5. Complaints and Whistle-blowing
|
||||
|
||||
7 The Corda Network Operator
|
||||
============================
|
||||
----------------------------
|
||||
|
||||
In order to pursue the mission of the Foundation as set out in section 1, there will need to be a set of operational
|
||||
activities, including technical activities such as hosting services, marketing activities, community management and
|
||||
@ -480,10 +480,10 @@ have been designed to be highly cacheable, and low-cost in operation.
|
||||
For the first three years, R3 shall act as the Operator.
|
||||
|
||||
8 Costs and Participation Fees
|
||||
==============================
|
||||
------------------------------
|
||||
|
||||
8.1 Costs
|
||||
---------
|
||||
^^^^^^^^^
|
||||
In line with the mission and values of the Foundation, the Network Foundation is not a profit seeking entity. But the
|
||||
Foundation needs to provide governance and technical services, and these will incur costs. The Foundation maintains these
|
||||
cost principles, as ideals but not contractual standards:
|
||||
@ -496,7 +496,7 @@ of all of its own administration, governance and technical services.
|
||||
5. The Foundation's cost model should be public, to demonstrate that the costs could not reasonably be lower.
|
||||
|
||||
8.2 Participation Fee
|
||||
---------------------
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
The Foundation shall meet costs by levying a participation fee and notary fee for all Participants. The participation
|
||||
fee will be independent of organisation size and number of transactions on Corda, to reflect the underlying cost of
|
||||
identity issuance.
|
||||
@ -520,13 +520,13 @@ The fee applies even if the Participants chooses not to operate a Corda node on
|
||||
can be potential or active participants.
|
||||
|
||||
8.3 Notary Fee
|
||||
--------------
|
||||
^^^^^^^^^^^^^^
|
||||
Transaction notary fees will be charged separately, on a per-use basis. This reflects the variable cost of providing
|
||||
notary services, with a wide orders-of-magnitude disparity between frequent and infrequent participant transaction
|
||||
volumes. As a principle, notary fees shall not subsidise participation fees, nor vice versa.
|
||||
|
||||
9 Community
|
||||
===========
|
||||
-----------
|
||||
Corda is a collaborative effort, and part of the Foundation’s mission is to help create and foster a technical community
|
||||
that will benefit all Corda solution providers and users. As such, the Foundation will work to encourage further
|
||||
participation of leading Participants of the ecosystem, including developers, service and solution providers and end
|
||||
@ -537,7 +537,7 @@ The Corda technical community should be broad and open, encouraging participatio
|
||||
technology and applications, but this cannot be mandated by the Foundation.
|
||||
|
||||
9.1 Non-Discrimination
|
||||
----------------------
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
The Foundation will welcome any organization able to meet the Participation criteria, regardless of competitive
|
||||
interests with other Participants. The Board shall not seek to exclude any Participant for any reasons other than those
|
||||
that are reasonable, explicit and applied on a non-discriminatory basis to all Participants.
|
||||
|
@ -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
|
||||
|
@ -1,5 +1,5 @@
|
||||
The Corda Testnet
|
||||
=================
|
||||
Joining Corda Testnet
|
||||
=====================
|
||||
|
||||
.. contents::
|
||||
|
||||
|
0
docs/source/deploy-locally.rst
Normal file
0
docs/source/deploy-locally.rst
Normal file
10
docs/source/deploy-to-testnet-index.rst
Normal file
10
docs/source/deploy-to-testnet-index.rst
Normal file
@ -0,0 +1,10 @@
|
||||
Deploying Corda to Testnet
|
||||
==========================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
azure-vm-explore
|
||||
aws-vm-explore
|
||||
gcp-vm
|
||||
deploy-locally
|
144
docs/source/design/linear-pointer/design.md
Normal file
144
docs/source/design/linear-pointer/design.md
Normal 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".
|
||||
|
||||
|
||||
|
||||
|
58
docs/source/joining-a-compatibility-zone.rst
Normal file
58
docs/source/joining-a-compatibility-zone.rst
Normal 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.
|
@ -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.
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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::
|
||||
|
||||
|
191
docs/source/setting-up-a-dynamic-compatibility-zone.rst
Normal file
191
docs/source/setting-up-a-dynamic-compatibility-zone.rst
Normal 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>`__.
|
@ -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
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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"))
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user