diff --git a/build.gradle b/build.gradle
index 98facae83d..5a841245ad 100644
--- a/build.gradle
+++ b/build.gradle
@@ -98,6 +98,7 @@ buildscript {
     ext.snappy_version = '0.4'
     ext.fast_classpath_scanner_version = '2.12.3'
     ext.jcabi_manifests_version = '1.1'
+    ext.picocli_version = '3.0.0'
 
     ext.deterministic_rt_version = '1.0-SNAPSHOT'
     // Name of the IntelliJ SDK created for the deterministic Java rt.jar.
diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/AMQPClientSerializationScheme.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/AMQPClientSerializationScheme.kt
index d6538d1ffc..389609f84c 100644
--- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/AMQPClientSerializationScheme.kt
+++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/AMQPClientSerializationScheme.kt
@@ -50,7 +50,7 @@ class AMQPClientSerializationScheme(
             target == SerializationContext.UseCase.RPCClient || target == SerializationContext.UseCase.P2P)
 
     override fun rpcClientSerializerFactory(context: SerializationContext): SerializerFactory {
-        return SerializerFactory(context.whitelist, ClassLoader.getSystemClassLoader()).apply {
+        return SerializerFactory(context.whitelist, ClassLoader.getSystemClassLoader(), context.lenientCarpenterEnabled).apply {
             register(RpcClientObservableSerializer)
             register(RpcClientCordaFutureSerializer(this))
             register(RxNotificationSerializer(this))
diff --git a/core/src/main/kotlin/net/corda/core/flows/NotaryWireFormat.kt b/core/src/main/kotlin/net/corda/core/flows/NotaryWireFormat.kt
index eff2ec41ad..6c3ca93ec2 100644
--- a/core/src/main/kotlin/net/corda/core/flows/NotaryWireFormat.kt
+++ b/core/src/main/kotlin/net/corda/core/flows/NotaryWireFormat.kt
@@ -71,12 +71,18 @@ data class NotarisationPayload(val transaction: Any, val requestSignature: Notar
      * A helper for automatically casting the underlying [transaction] payload to a [SignedTransaction].
      * Should only be used by validating notaries.
      */
-    val signedTransaction get() = transaction as SignedTransaction
+    val signedTransaction get() = transaction as? SignedTransaction ?: throw exception()
+
     /**
      * A helper for automatically casting the underlying [transaction] payload to a [CoreTransaction].
      * Should only be used by non-validating notaries.
      */
-    val coreTransaction get() = transaction as CoreTransaction
+    val coreTransaction get() = transaction as? CoreTransaction ?: throw exception()
+
+    private fun exception() = IllegalArgumentException("Unexpected transaction type in the notarisation payload: " +
+            "${transaction::class.java}, it may be that there is a discrepancy between the configured notary type " +
+            "(validating/non-validating) and the one advertised on the network parameters."
+    )
 }
 
 /** Payload returned by the notary service flow to the client. */
diff --git a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt
index 8262373dea..13296e70b3 100644
--- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt
+++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt
@@ -24,6 +24,7 @@ import net.corda.core.flows.FlowLogic
 import net.corda.core.identity.CordaX500Name
 import net.corda.core.node.ServicesForResolution
 import net.corda.core.serialization.*
+import net.corda.core.transactions.LedgerTransaction
 import net.corda.core.transactions.SignedTransaction
 import net.corda.core.transactions.TransactionBuilder
 import net.corda.core.transactions.WireTransaction
@@ -396,12 +397,19 @@ fun <T, U : T> uncheckedCast(obj: T) = obj as U
 fun <K, V> Iterable<Pair<K, V>>.toMultiMap(): Map<K, List<V>> = this.groupBy({ it.first }) { it.second }
 
 /** Provide access to internal method for AttachmentClassLoaderTests */
-@DeleteForDJVM fun TransactionBuilder.toWireTransaction(services: ServicesForResolution, serializationContext: SerializationContext): WireTransaction {
+@DeleteForDJVM
+fun TransactionBuilder.toWireTransaction(services: ServicesForResolution, serializationContext: SerializationContext): WireTransaction {
     return toWireTransactionWithContext(services, serializationContext)
 }
 
 /** Provide access to internal method for AttachmentClassLoaderTests */
-@DeleteForDJVM fun TransactionBuilder.toLedgerTransaction(services: ServicesForResolution, serializationContext: SerializationContext) = toLedgerTransactionWithContext(services, serializationContext)
+@DeleteForDJVM
+fun TransactionBuilder.toLedgerTransaction(services: ServicesForResolution, serializationContext: SerializationContext): LedgerTransaction {
+    return toLedgerTransactionWithContext(services, serializationContext)
+}
+
+/** Returns the location of this class. */
+val Class<*>.location: URL get() = protectionDomain.codeSource.location
 
 /** Convenience method to get the package name of a class literal. */
 val KClass<*>.packageName: String get() = java.packageName
diff --git a/core/src/main/kotlin/net/corda/core/internal/PathUtils.kt b/core/src/main/kotlin/net/corda/core/internal/PathUtils.kt
index 7ef532e06b..50e05c06b6 100644
--- a/core/src/main/kotlin/net/corda/core/internal/PathUtils.kt
+++ b/core/src/main/kotlin/net/corda/core/internal/PathUtils.kt
@@ -81,6 +81,9 @@ fun Path.lastModifiedTime(vararg options: LinkOption): FileTime = Files.getLastM
 /** @see Files.isDirectory */
 fun Path.isDirectory(vararg options: LinkOption): Boolean = Files.isDirectory(this, *options)
 
+/** @see Files.isSameFile */
+fun Path.isSameAs(other: Path): Boolean = Files.isSameFile(this, other)
+
 /**
  * Same as [Files.list] except it also closes the [Stream].
  * @return the output of [block]
diff --git a/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt b/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt
index e3b85247d5..e21cc456dd 100644
--- a/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt
+++ b/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt
@@ -152,6 +152,14 @@ interface SerializationContext {
      * otherwise they appear as new copies of the object.
      */
     val objectReferencesEnabled: Boolean
+    /**
+     * If true the carpenter will happily synthesis classes that implement interfaces containing methods that are not
+     * getters for any AMQP fields. Invoking these methods will throw an [AbstractMethodError]. If false then an exception
+     * will be thrown during deserialization instead.
+     *
+     * The default is false.
+     */
+    val lenientCarpenterEnabled: Boolean
     /**
      * The use case we are serializing or deserializing for.  See [UseCase].
      */
@@ -167,6 +175,12 @@ interface SerializationContext {
      */
     fun withoutReferences(): SerializationContext
 
+    /**
+     * Return a new context based on this one but with a lenient carpenter.
+     * @see lenientCarpenterEnabled
+     */
+    fun withLenientCarpenter(): SerializationContext
+
     /**
      * Helper method to return a new context based on this context with the deserialization class loader changed.
      */
diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst
index 15cd8c2a69..9208645842 100644
--- a/docs/source/changelog.rst
+++ b/docs/source/changelog.rst
@@ -9,6 +9,10 @@ Unreleased
 
 * Introduced a hierarchy of ``DatabaseMigrationException``s, allowing ``NodeStartup`` to gracefully inform users of problems related to database migrations before exiting with a non-zero code.
 
+* The class carpenter has a "lenient" mode where it will, during deserialisation, happily synthesis classes that implement
+  interfaces that will have unimplemented methods. This is useful, for example, for object viewers. This can be turned on
+  with ``SerializationContext.withLenientCarpenter``.
+
 * Introduced a grace period before the initial node registration fails if the node cannot connect to the Doorman.
   It retries 10 times with a 1 minute interval in between each try. At the moment this is not configurable.
 
@@ -273,7 +277,7 @@ Corda Enterprise 3.0 Developer Preview
    * Cordform (which is the ``deployNodes`` gradle task) does this copying automatically for the demos. The ``NetworkMap``
      parameter is no longer needed.
 
-   * For test deployments we've introduced a bootstrapping tool (see :doc:`setting-up-a-corda-network`).
+   * For test deployments we've introduced a bootstrapping tool (see :doc:`network-bootstrapper`).
 
    * ``extraAdvertisedServiceIds``, ``notaryNodeAddress``, ``notaryClusterAddresses`` and ``bftSMaRt`` configs have been
      removed. The configuration of notaries has been simplified into a single ``notary`` config object. See
diff --git a/docs/source/conf.py b/docs/source/conf.py
index ba3ed0bd61..5cc14c8a7c 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -238,6 +238,14 @@ latex_documents = [('index', u'corda-developer-site.tex', u'Corda Developer Docu
 # If false, no module index is generated.
 # latex_domain_indices = True
 
+_PREAMBLE = r"""
+\usepackage[utf8]{inputenc}
+\usepackage{pmboxdraw}
+\DeclareUnicodeCharacter{2514}{\textSFii}
+\DeclareUnicodeCharacter{251C}{\textSFviii}
+\DeclareUnicodeCharacter{2705}{\checkmark}
+"""
+
 latex_elements = {
     # The paper size ('letterpaper' or 'a4paper').
     # 'papersize': 'letterpaper',
@@ -251,7 +259,9 @@ latex_elements = {
     # Latex figure (float) alignment
     # 'figure_align': 'htbp',
 
-    'maxlistdepth': 2000,
+    'maxlistdepth' : '2000',
 
-    'extraclassoptions': 'openany',
+    'preamble' : _PREAMBLE,
+
+    'extraclassoptions' : 'openany',
 }
diff --git a/docs/source/network-bootstrapper.rst b/docs/source/network-bootstrapper.rst
new file mode 100644
index 0000000000..e0c3900927
--- /dev/null
+++ b/docs/source/network-bootstrapper.rst
@@ -0,0 +1,101 @@
+Network Bootstrapper
+====================
+
+Test deployments
+~~~~~~~~~~~~~~~~
+
+Nodes within a network see each other using the network map. This is a collection of statically signed node-info files,
+one for each node. Most production deployments will use a highly available, secure distribution of the network map via HTTP.
+
+For test deployments where the nodes (at least initially) reside on the same filesystem, these node-info files can be
+placed directly in the node's ``additional-node-infos`` directory from where the node will pick them up and store them
+in its local network map cache. The node generates its own node-info file on startup.
+
+In addition to the network map, all the nodes must also use the same set of network parameters. These are a set of constants
+which guarantee interoperability between the nodes. The HTTP network map distributes the network parameters which are downloaded
+automatically by the nodes. In the absence of this the network parameters must be generated locally.
+
+For these reasons, test deployments can avail themselves of the network bootstrapper. This is a tool that scans all the
+node configurations from a common directory to generate the network parameters file, which is then copied to all the nodes'
+directories. It also copies each node's node-info file to every other node so that they can all be visible to each other.
+
+You can find out more about network maps and network parameters from :doc:`network-map`.
+
+Bootstrapping a test network
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The bootstrapper can be downloaded from https://downloads.corda.net/network-bootstrapper-VERSION.jar, where ``VERSION``
+is the Corda version.
+
+Create a directory containing a node config file, ending in "_node.conf", for each node you want to create. Then run the
+following command:
+
+``java -jar network-bootstrapper-VERSION.jar --dir <nodes-root-dir>``
+
+For example running the command on a directory containing these files:
+
+.. sourcecode:: none
+
+    .
+    ├── notary_node.conf             // The notary's node.conf file
+    ├── partya_node.conf             // Party A's node.conf file
+    └── partyb_node.conf             // Party B's node.conf file
+
+will generate directories containing three nodes: ``notary``, ``partya`` and ``partyb``. They will each use the ``corda.jar``
+that comes with the bootstrapper. If a different version of Corda is required then simply place that ``corda.jar`` file
+alongside the configuration files in the directory.
+
+The directory can also contain CorDapp JARs which will be copied to each node's ``cordapps`` directory.
+
+You can also have the node directories containing their "node.conf" files already laid out. The previous example would be:
+
+.. sourcecode:: none
+
+    .
+    ├── notary
+    │   └── node.conf
+    ├── partya
+    │   └── node.conf
+    └── partyb
+        └── node.conf
+
+Similarly, each node directory may contain its own ``corda.jar``, which the bootstrapper will use instead.
+
+Synchronisation
+~~~~~~~~~~~~~~~
+
+This tool only bootstraps a network. It cannot dynamically update if a new node needs to join the network or if an existing
+one has changed something in their node-info, e.g. their P2P address. For this the new node-info file will need to be placed
+in the other nodes' ``additional-node-infos`` directory. A simple way to do this is to use `rsync <https://en.wikipedia.org/wiki/Rsync>`_.
+However, if it's known beforehand the set of nodes that will eventually form part of the network then all the node directories
+can be pregenerated in the bootstrap and only started when needed.
+
+Running the bootstrapper again on the same network will allow a new node to be added or an existing one to have its updated
+node-info re-distributed. However this comes at the expense of having to temporarily collect the node directories back
+together again under a common parent directory.
+
+Whitelisting contracts
+~~~~~~~~~~~~~~~~~~~~~~
+
+The CorDapp JARs are also automatically used to create the *Zone whitelist* (see :doc:`api-contract-constraints`) for
+the network.
+
+.. note:: If you only wish to whitelist the CorDapps but not copy them to each node then run with the ``--no-copy`` flag.
+
+The CorDapp JARs will be hashed and scanned for ``Contract`` classes. These contract class implementations will become part
+of the whitelisted contracts in the network parameters (see ``NetworkParameters.whitelistedContractImplementations`` :doc:`network-map`).
+If the network already has a set of network parameters defined (i.e. the node directories all contain the same network-parameters
+file) then the new set of contracts will be appended to the current whitelist.
+
+.. note:: The whitelist can only ever be appended to. Once added a contract implementation can never be removed.
+
+By default the bootstrapper will whitelist all the contracts found in all the CorDapp JARs. To prevent certain
+contracts from being whitelisted, add their fully qualified class name in the ``exclude_whitelist.txt``. These will instead
+use the more restrictive ``HashAttachmentConstraint``.
+
+For example:
+
+.. sourcecode:: none
+
+    net.corda.finance.contracts.asset.Cash
+    net.corda.finance.contracts.asset.CommercialPaper
diff --git a/docs/source/network-map.rst b/docs/source/network-map.rst
index 72c1d00dd4..001dc57e90 100644
--- a/docs/source/network-map.rst
+++ b/docs/source/network-map.rst
@@ -72,7 +72,7 @@ the network, along with the network parameters file and identity certificates. G
 online at once - an offline node that isn't being interacted with doesn't impact the network in any way. So a test
 cluster generated like this can be sized for the maximum size you may need, and then scaled up and down as necessary.
 
-More information can be found in :doc:`setting-up-a-corda-network`.
+More information can be found in :doc:`network-bootstrapper`.
 
 Network parameters
 ------------------
diff --git a/docs/source/serialization.rst b/docs/source/serialization.rst
index e62a129255..239d420d52 100644
--- a/docs/source/serialization.rst
+++ b/docs/source/serialization.rst
@@ -564,6 +564,11 @@ without the supporting classes being present on the classpath.  This can be usef
 be able to use reflection over the deserialized data, for scripting languages that run on the JVM, and also for
 ensuring classes not on the classpath can be deserialized without loading potentially malicious code.
 
+If the original class implements some interfaces then the carpenter will make sure that all of the interface methods are
+backed by feilds. If that's not the case then an exception will be thrown during deserialization. This check can
+be turned off with ``SerializationContext.withLenientCarpenter``. This can be useful if only the field getters are needed,
+say in an object viewer.
+
 Possible future enhancements include:
 
     #.  Java singleton support.  We will add support for identifying classes which are singletons and identifying the
diff --git a/docs/source/setting-up-a-corda-network.rst b/docs/source/setting-up-a-corda-network.rst
index ce0d70a8b6..b8946a2841 100644
--- a/docs/source/setting-up-a-corda-network.rst
+++ b/docs/source/setting-up-a-corda-network.rst
@@ -46,81 +46,14 @@ The most important fields regarding network configuration are:
   and ``rpcAddress`` if they are on the same machine.
 * ``notary.serviceLegalName``: The name of the notary service, required to setup distributed notaries with the network-bootstrapper.
 
-Bootstrapping the network
-~~~~~~~~~~~~~~~~~~~~~~~~~
-
-The nodes see each other using the network map. This is a collection of statically signed node-info files, one for each
-node in the network. Most production deployments will use a highly available, secure distribution of the network map via HTTP.
-
-For test deployments where the nodes (at least initially) reside on the same filesystem, these node-info files can be
-placed directly in the node's ``additional-node-infos`` directory from where the node will pick them up and store them
-in its local network map cache. The node generates its own node-info file on startup.
-
-In addition to the network map, all the nodes on a network must use the same set of network parameters. These are a set
-of constants which guarantee interoperability between nodes. The HTTP network map distributes the network parameters
-which the node downloads automatically. In the absence of this the network parameters must be generated locally. This can
-be done with the network bootstrapper. This is a tool that scans all the node configurations from a common directory to
-generate the network parameters file which is copied to the nodes' directories. It also copies each node's node-info file
-to every other node so that they can all transact with each other.
-
-The bootstrapper tool can be downloaded from https://downloads.corda.net/network-bootstrapper-corda-X.Y.jar, where ``X``
-is the major Corda version and ``Y`` is the minor Corda version.
-
-To use it, create a directory containing a node config file, ending in "_node.conf", for each node you want to create.
-Then run the following command:
-
-``java -jar network-bootstrapper-corda-X.Y.jar <nodes-root-dir>``
-
-For example running the command on a directory containing these files :
-
-.. sourcecode:: none
-
-    .
-    ├── notary_node.conf             // The notary's node.conf file
-    ├── partya_node.conf             // Party A's node.conf file
-    └── partyb_node.conf             // Party B's node.conf file
-
-Would generate directories containing three nodes: notary, partya and partyb.
-
-This tool only bootstraps a network. It cannot dynamically update if a new node needs to join the network or if an existing
-one has changed something in their node-info, e.g. their P2P address. For this the new node-info file will need to be placed
-in the other nodes' ``additional-node-infos`` directory. A simple way to do this is to use `rsync <https://en.wikipedia.org/wiki/Rsync>`_.
-However, if it's known beforehand the set of nodes that will eventually the node folders can be pregenerated in the bootstrap
-and only started when needed.
-
-Whitelisting Contracts
-~~~~~~~~~~~~~~~~~~~~~~
-
-If you want to create a *Zone whitelist* (see :doc:`api-contract-constraints`), you can pass in a list of CorDapp jars:
-
-``java -jar network-bootstrapper.jar <nodes-root-dir> <1st CorDapp jar> <2nd CorDapp jar> ..``
-
-The CorDapp jars will be hashed and scanned for ``Contract`` classes. These contract class implementations will become part
-of the whitelisted contracts in the network parameters (see ``NetworkParameters.whitelistedContractImplementations`` :doc:`network-map`).
-If the network already has a set of network parameters defined (i.e. the node directories all contain the same network-parameters
-file) then the new set of contracts will be appended to the current whitelist.
-
-.. note:: The whitelist can only ever be appended to. Once added a contract implementation can never be removed.
-
-By default the bootstrapper tool will whitelist all the contracts found in all the CorDapp jars. To prevent certain
-contracts from being whitelisted, add their fully qualified class name in the ``exclude_whitelist.txt``. These will instead
-use the more restrictive ``HashAttachmentConstraint``.
-
-For example:
-
-.. sourcecode:: none
-
-    net.corda.finance.contracts.asset.Cash
-    net.corda.finance.contracts.asset.CommercialPaper
-
-In addition to using the CorDapp jars to update the whitelist, the bootstrapper will also copy them to all the nodes'
-``cordapps`` directory.
-
 Starting the nodes
 ~~~~~~~~~~~~~~~~~~
 
-You may now start the nodes in any order. You should see a banner, some log lines and eventually ``Node started up and registered``,
-indicating that the node is fully started.
+You will first need to create the local network by bootstrapping it with the bootstrapper. Details of how to do that can
+be found in :doc:`network-bootstrapper`.
+
+Once that's done you may now start the nodes in any order. You should see a banner, some log lines and eventually
+``Node started up and registered``, indicating that the node is fully started.
 
 .. TODO: Add a better way of polling for startup. A programmatic way of determining whether a node is up is to check whether it's ``webAddress`` is bound.
 
diff --git a/docs/source/tools-index.rst b/docs/source/tools-index.rst
index f2213c7496..2285b38d9d 100644
--- a/docs/source/tools-index.rst
+++ b/docs/source/tools-index.rst
@@ -4,6 +4,7 @@ Tools
 .. toctree::
    :maxdepth: 1
 
+   network-bootstrapper
    blob-inspector
    demobench
    node-explorer
diff --git a/docs/source/tutorial-cordapp.rst b/docs/source/tutorial-cordapp.rst
index 2c2447d448..961b17c6dd 100644
--- a/docs/source/tutorial-cordapp.rst
+++ b/docs/source/tutorial-cordapp.rst
@@ -418,7 +418,7 @@ be moved to another machine open its config file and change the Artemis messagin
 where the node will run (e.g. ``p2pAddress="10.18.0.166:10007"``).
 
 These changes require new node-info files to be distributed amongst the nodes. Use the network bootstrapper tool
-(see :doc:`setting-up-a-corda-network` for more information on this and how to built it) to update the files and have
+(see :doc:`network-bootstrapper` for more information on this and how to built it) to update the files and have
 them distributed locally.
 
 ``java -jar network-bootstrapper.jar kotlin-source/build/nodes``
diff --git a/docs/source/upgrade-notes.rst b/docs/source/upgrade-notes.rst
index 851df2a5a5..505fcb2bd6 100644
--- a/docs/source/upgrade-notes.rst
+++ b/docs/source/upgrade-notes.rst
@@ -135,7 +135,7 @@ With the re-designed network map service the following changes need to be made:
 * The network map is no longer provided by a node and thus the ``networkMapService`` config is ignored. Instead the
   network map is either provided by the compatibility zone (CZ) operator (who operates the doorman) and available
   using the ``compatibilityZoneURL`` config, or is provided using signed node info files which are copied locally.
-  See :doc:`network-map` for more details, and :doc:`setting-up-a-corda-network` on how to use the network
+  See :doc:`network-map` for more details, and :doc:`network-bootstrapper` on how to use the network
   bootstrapper for deploying a local network.
 
 * Configuration for a notary has been simplified. ``extraAdvertisedServiceIds``, ``notaryNodeAddress``, ``notaryClusterAddresses``
diff --git a/experimental/behave/prepare.sh b/experimental/behave/prepare.sh
index 2ab409b448..502c38d962 100755
--- a/experimental/behave/prepare.sh
+++ b/experimental/behave/prepare.sh
@@ -50,7 +50,7 @@ curl -L "http://central.maven.org/maven2/org/postgresql/postgresql/42.1.4/postgr
 curl -L "https://github.com/Microsoft/mssql-jdbc/releases/download/v6.2.2/mssql-jdbc-6.2.2.jre8.jar" > ${DRIVERS_DIR}/mssql-jdbc-6.2.2.jre8.jar
 
 # Build Network Bootstrapper
-./gradlew buildBootstrapperJar
+./gradlew tools:bootstrapper:jar
 cp -v $(ls tools/bootstrapper/build/libs/*.jar | tail -n1) ${CORDA_DIR}/network-bootstrapper.jar
 
 # TODO: resolve Doorman/NMS artifacts from new artifactory location.
diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt
index bf19d22802..9ec44e109b 100644
--- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt
+++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt
@@ -37,8 +37,8 @@ import net.corda.serialization.internal.CordaSerializationMagic
 import net.corda.serialization.internal.SerializationFactoryImpl
 import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme
 import net.corda.serialization.internal.amqp.amqpMagic
+import java.io.InputStream
 import java.nio.file.Path
-import java.nio.file.Paths
 import java.nio.file.StandardCopyOption.REPLACE_EXISTING
 import java.time.Instant
 import java.util.*
@@ -53,7 +53,21 @@ import kotlin.streams.toList
 /**
  * Class to bootstrap a local network of Corda nodes on the same filesystem.
  */
-class NetworkBootstrapper {
+// TODO Move this to tools:bootstrapper
+class NetworkBootstrapper
+    @VisibleForTesting
+    internal constructor(private val initSerEnv: Boolean,
+                         private val embeddedCordaJar: () -> InputStream,
+                         private val nodeInfosGenerator: (List<Path>) -> List<Path>,
+                         private val contractsJarConverter: (Path) -> ContractsJar) {
+
+    constructor() : this(
+            initSerEnv = true,
+            embeddedCordaJar = Companion::extractEmbeddedCordaJar,
+            nodeInfosGenerator = Companion::generateNodeInfos,
+            contractsJarConverter = ::ContractsJarFile
+    )
+
     companion object {
         // TODO This will probably need to change once we start using a bundled JVM
         private val nodeInfoGenCmd = listOf(
@@ -65,11 +79,42 @@ class NetworkBootstrapper {
 
         private const val LOGS_DIR_NAME = "logs"
 
-        @JvmStatic
-        fun main(args: Array<String>) {
-            val baseNodeDirectory = requireNotNull(args.firstOrNull()) { "Expecting first argument which is the nodes' parent directory" }
-            val cordappJars = if (args.size > 1) args.asList().drop(1).map { Paths.get(it) } else emptyList()
-            NetworkBootstrapper().bootstrap(Paths.get(baseNodeDirectory).toAbsolutePath().normalize(), cordappJars)
+        private fun extractEmbeddedCordaJar(): InputStream {
+            return Thread.currentThread().contextClassLoader.getResourceAsStream("corda.jar")
+        }
+
+        private fun generateNodeInfos(nodeDirs: List<Path>): List<Path> {
+            val numParallelProcesses = Runtime.getRuntime().availableProcessors()
+            val timePerNode = 40.seconds // On the test machine, generating the node info takes 7 seconds for a single node.
+            val tExpected = maxOf(timePerNode, timePerNode * nodeDirs.size.toLong() / numParallelProcesses.toLong())
+            val warningTimer = Timer("WarnOnSlowMachines", false).schedule(tExpected.toMillis()) {
+                println("... still waiting. If this is taking longer than usual, check the node logs.")
+            }
+            val executor = Executors.newFixedThreadPool(numParallelProcesses)
+            return try {
+                nodeDirs.map { executor.fork { generateNodeInfo(it) } }.transpose().getOrThrow()
+            } finally {
+                warningTimer.cancel()
+                executor.shutdownNow()
+            }
+        }
+
+        private fun generateNodeInfo(nodeDir: Path): Path {
+            val logsDir = (nodeDir / LOGS_DIR_NAME).createDirectories()
+            val process = ProcessBuilder(nodeInfoGenCmd)
+                    .directory(nodeDir.toFile())
+                    .redirectErrorStream(true)
+                    .redirectOutput((logsDir / "node-info-gen.log").toFile())
+                    .apply { environment()["CAPSULE_CACHE_DIR"] = "../.cache" }
+                    .start()
+            if (!process.waitFor(3, TimeUnit.MINUTES)) {
+                process.destroyForcibly()
+                throw IllegalStateException("Error while generating node info file. Please check the logs in $logsDir.")
+            }
+            check(process.exitValue() == 0) {  "Error while generating node info file. Please check the logs in $logsDir." }
+            return nodeDir.list { paths ->
+                paths.filter { it.fileName.toString().startsWith(NODE_INFO_FILE_NAME_PREFIX) }.findFirst().get()
+            }
         }
     }
 
@@ -102,29 +147,61 @@ class NetworkBootstrapper {
     private fun generateServiceIdentitiesForNotaryClusters(configs: Map<Path, Config>) {
         notaryClusters(configs).forEach { (cluster, directories) ->
             when (cluster) {
-                is NotaryCluster.BFT ->
-                    DevIdentityGenerator.generateDistributedNotaryCompositeIdentity(directories, cluster.name, threshold = 1 + 2 * directories.size / 3)
-                is NotaryCluster.CFT ->
-                    DevIdentityGenerator.generateDistributedNotarySingularIdentity(directories, cluster.name)
+                is NotaryCluster.BFT -> DevIdentityGenerator.generateDistributedNotaryCompositeIdentity(
+                        directories,
+                        cluster.name,
+                        threshold = 1 + 2 * directories.size / 3
+                )
+                is NotaryCluster.CFT -> DevIdentityGenerator.generateDistributedNotarySingularIdentity(directories, cluster.name)
             }
         }
     }
 
+    /** Entry point for Cordform */
     fun bootstrap(directory: Path, cordappJars: List<Path>) {
+        bootstrap(directory, cordappJars, copyCordapps = true, fromCordform = true)
+    }
+
+    /** Entry point for the tool */
+    fun bootstrap(directory: Path, copyCordapps: Boolean) {
+        // 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)
+    }
+
+    private fun bootstrap(directory: Path, cordappJars: List<Path>, copyCordapps: Boolean, fromCordform: Boolean) {
         directory.createDirectories()
-        println("Bootstrapping local network in $directory")
-        generateDirectoriesIfNeeded(directory, cordappJars)
-        val nodeDirs = directory.list { paths -> paths.filter { (it / "corda.jar").exists() }.toList() }
+        println("Bootstrapping local test network in $directory")
+        if (!fromCordform) {
+            println("Found the following CorDapps: ${cordappJars.map { it.fileName }}")
+        }
+        createNodeDirectoriesIfNeeded(directory, fromCordform)
+        val nodeDirs = gatherNodeDirectories(directory)
+
         require(nodeDirs.isNotEmpty()) { "No nodes found" }
-        println("Nodes found in the following sub-directories: ${nodeDirs.map { it.fileName }}")
+        if (!fromCordform) {
+            println("Nodes found in the following sub-directories: ${nodeDirs.map { it.fileName }}")
+        }
+
         val configs = nodeDirs.associateBy({ it }, { ConfigFactory.parseFile((it / "node.conf").toFile()) })
+        checkForDuplicateLegalNames(configs.values)
+        if (copyCordapps && cordappJars.isNotEmpty()) {
+            println("Copying CorDapp JARs into node directories")
+            for (nodeDir in nodeDirs) {
+                val cordappsDir = (nodeDir / "cordapps").createDirectories()
+                cordappJars.forEach { it.copyToDirectory(cordappsDir) }
+            }
+        }
         generateServiceIdentitiesForNotaryClusters(configs)
-        initialiseSerialization()
+        if (initSerEnv) {
+            initialiseSerialization()
+        }
         try {
             println("Waiting for all nodes to generate their node-info files...")
-            val nodeInfoFiles = generateNodeInfos(nodeDirs)
-            println("Checking for duplicate nodes")
-            checkForDuplicateLegalNames(nodeInfoFiles)
+            val nodeInfoFiles = nodeInfosGenerator(nodeDirs)
             println("Distributing all node-info files to all nodes")
             distributeNodeInfos(nodeDirs, nodeInfoFiles)
             print("Loading existing network parameters... ")
@@ -133,72 +210,70 @@ class NetworkBootstrapper {
             println("Gathering notary identities")
             val notaryInfos = gatherNotaryInfos(nodeInfoFiles, configs)
             println("Generating contract implementations whitelist")
-            val newWhitelist = generateWhitelist(existingNetParams, readExcludeWhitelist(directory), cordappJars.map(::ContractsJarFile))
-            val netParams = installNetworkParameters(notaryInfos, newWhitelist, existingNetParams, nodeDirs)
-            println("${if (existingNetParams == null) "New" else "Updated"} $netParams")
+            val newWhitelist = generateWhitelist(existingNetParams, readExcludeWhitelist(directory), cordappJars.map(contractsJarConverter))
+            val newNetParams = installNetworkParameters(notaryInfos, newWhitelist, existingNetParams, nodeDirs)
+            if (newNetParams != existingNetParams) {
+                println("${if (existingNetParams == null) "New" else "Updated"} $newNetParams")
+            } else {
+                println("Network parameters unchanged")
+            }
             println("Bootstrapping complete!")
         } finally {
-            _contextSerializationEnv.set(null)
+            if (initSerEnv) {
+                _contextSerializationEnv.set(null)
+            }
         }
     }
 
-    private fun generateNodeInfos(nodeDirs: List<Path>): List<Path> {
-        val numParallelProcesses = Runtime.getRuntime().availableProcessors()
-        val timePerNode = 40.seconds // On the test machine, generating the node info takes 7 seconds for a single node.
-        val tExpected = maxOf(timePerNode, timePerNode * nodeDirs.size.toLong() / numParallelProcesses.toLong())
-        val warningTimer = Timer("WarnOnSlowMachines", false).schedule(tExpected.toMillis()) {
-            println("...still waiting. If this is taking longer than usual, check the node logs.")
+    private fun createNodeDirectoriesIfNeeded(directory: Path, fromCordform: Boolean) {
+        val cordaJar = directory / "corda.jar"
+        var usingEmbedded = false
+        if (!cordaJar.exists()) {
+            embeddedCordaJar().use { it.copyTo(cordaJar) }
+            usingEmbedded = true
+        } else if (!fromCordform) {
+            println("Using corda.jar in root directory")
         }
-        val executor = Executors.newFixedThreadPool(numParallelProcesses)
-        return try {
-            nodeDirs.map { executor.fork { generateNodeInfo(it) } }.transpose().getOrThrow()
-        } finally {
-            warningTimer.cancel()
-            executor.shutdownNow()
-        }
-    }
 
-    private fun generateNodeInfo(nodeDir: Path): Path {
-        val logsDir = (nodeDir / LOGS_DIR_NAME).createDirectories()
-        val process = ProcessBuilder(nodeInfoGenCmd)
-                .directory(nodeDir.toFile())
-                .redirectErrorStream(true)
-                .redirectOutput((logsDir / "node-info-gen.log").toFile())
-                .apply { environment()["CAPSULE_CACHE_DIR"] = "../.cache" }
-                .start()
-        if (!process.waitFor(3, TimeUnit.MINUTES)) {
-            process.destroyForcibly()
-            throw IllegalStateException("Error while generating node info file. Please check the logs in $logsDir.")
-        }
-        check(process.exitValue() == 0) {  "Error while generating node info file. Please check the logs in $logsDir." }
-        return nodeDir.list { paths -> paths.filter { it.fileName.toString().startsWith(NODE_INFO_FILE_NAME_PREFIX) }.findFirst().get() }
-    }
-
-    private fun generateDirectoriesIfNeeded(directory: Path, cordappJars: List<Path>) {
         val confFiles = directory.list { it.filter { it.toString().endsWith("_node.conf") }.toList() }
         val webServerConfFiles = directory.list { it.filter { it.toString().endsWith("_web-server.conf") }.toList() }
-        if (confFiles.isEmpty()) return
-        println("Node config files found in the root directory - generating node directories and copying CorDapp jars into them")
-        val cordaJar = extractCordaJarTo(directory)
+
         for (confFile in confFiles) {
             val nodeName = confFile.fileName.toString().removeSuffix("_node.conf")
-            println("Generating directory for $nodeName")
+            println("Generating node directory for $nodeName")
             val nodeDir = (directory / nodeName).createDirectories()
-            confFile.moveTo(nodeDir / "node.conf", REPLACE_EXISTING)
-            webServerConfFiles.firstOrNull { directory.relativize(it).toString().removeSuffix("_web-server.conf") == nodeName }?.moveTo(nodeDir / "web-server.conf", REPLACE_EXISTING)
+            confFile.copyTo(nodeDir / "node.conf", REPLACE_EXISTING)
+            webServerConfFiles.firstOrNull { directory.relativize(it).toString().removeSuffix("_web-server.conf") == nodeName }?.copyTo(nodeDir / "web-server.conf", REPLACE_EXISTING)
             cordaJar.copyToDirectory(nodeDir, REPLACE_EXISTING)
-            val cordappsDir = (nodeDir / "cordapps").createDirectories()
-            cordappJars.forEach { it.copyToDirectory(cordappsDir) }
         }
-        cordaJar.delete()
+
+        directory.list { paths ->
+            paths.filter { (it / "node.conf").exists() && !(it / "corda.jar").exists() }.forEach {
+                println("Copying corda.jar into node directory ${it.fileName}")
+                cordaJar.copyToDirectory(it)
+            }
+        }
+
+        if (fromCordform) {
+            confFiles.forEach(Path::delete)
+            webServerConfFiles.forEach(Path::delete)
+        }
+
+        if (fromCordform || usingEmbedded) {
+            cordaJar.delete()
+        }
     }
 
-    private fun extractCordaJarTo(directory: Path): Path {
-        val cordaJarPath = directory / "corda.jar"
-        if (!cordaJarPath.exists()) {
-            Thread.currentThread().contextClassLoader.getResourceAsStream("corda.jar").use { it.copyTo(cordaJarPath) }
+    private fun gatherNodeDirectories(directory: Path): List<Path> {
+        return directory.list { paths ->
+            paths.filter {
+                val exists = (it / "corda.jar").exists()
+                if (exists) {
+                    require((it / "node.conf").exists()) { "Missing node.conf in node directory ${it.fileName}" }
+                }
+                exists
+            }.toList()
         }
-        return cordaJarPath
     }
 
     private fun distributeNodeInfos(nodeDirs: List<Path>, nodeInfoFiles: List<Path>) {
@@ -210,20 +285,13 @@ class NetworkBootstrapper {
         }
     }
 
-    /*the function checks for duplicate myLegalName in the all the *_node.conf files
-    All the myLegalName values are added to a HashSet - this helps detect duplicate values.
-    If a duplicate name is found the process is aborted with an error message
-    */
-    private fun checkForDuplicateLegalNames(nodeInfoFiles: List<Path>) {
-      val legalNames = HashSet<String>()
-      for (nodeInfoFile in nodeInfoFiles) {
-        val nodeConfig = ConfigFactory.parseFile((nodeInfoFile.parent / "node.conf").toFile())
-        val legalName = nodeConfig.getString("myLegalName")
-        if(!legalNames.add(legalName)){
-          println("Duplicate Node Found - ensure every node has a unique legal name");
-          throw IllegalArgumentException("Duplicate Node Found - $legalName");
+    private fun checkForDuplicateLegalNames(nodeConfigs: Collection<Config>) {
+        val duplicateLegalNames = nodeConfigs
+                .groupBy { it.getString("myLegalName") }
+                .mapNotNull { if (it.value.size > 1) it.key else null }
+        check(duplicateLegalNames.isEmpty()) {
+            "Nodes must have unique legal names. The following are used more than once: $duplicateLegalNames"
         }
-      }
     }
 
     private fun gatherNotaryInfos(nodeInfoFiles: List<Path>, configs: Map<Path, Config>): List<NotaryInfo> {
@@ -274,15 +342,19 @@ class NetworkBootstrapper {
                                          whitelist: Map<String, List<AttachmentId>>,
                                          existingNetParams: NetworkParameters?,
                                          nodeDirs: List<Path>): NetworkParameters {
-        val networkParameters = if (existingNetParams != null) {
-            existingNetParams.copy(
-                    notaries = notaryInfos,
-                    modifiedTime = Instant.now(),
-                    whitelistedContractImplementations = whitelist,
-                    epoch = existingNetParams.epoch + 1
-            )
+        // TODO Add config for minimumPlatformVersion, maxMessageSize and maxTransactionSize
+        val netParams = if (existingNetParams != null) {
+            if (existingNetParams.whitelistedContractImplementations == whitelist && existingNetParams.notaries == notaryInfos) {
+                existingNetParams
+            } else {
+                existingNetParams.copy(
+                        notaries = notaryInfos,
+                        modifiedTime = Instant.now(),
+                        whitelistedContractImplementations = whitelist,
+                        epoch = existingNetParams.epoch + 1
+                )
+            }
         } else {
-            // TODO Add config for minimumPlatformVersion, maxMessageSize and maxTransactionSize
             NetworkParameters(
                     minimumPlatformVersion = 1,
                     notaries = notaryInfos,
@@ -294,9 +366,9 @@ class NetworkBootstrapper {
                     eventHorizon = 30.days
             )
         }
-        val copier = NetworkParametersCopier(networkParameters, overwriteFile = true)
+        val copier = NetworkParametersCopier(netParams, overwriteFile = true)
         nodeDirs.forEach(copier::install)
-        return networkParameters
+        return netParams
     }
 
     private fun NodeInfo.notaryIdentity(): Party {
@@ -311,7 +383,6 @@ class NetworkBootstrapper {
     }
 
     // We need to to set serialization env, because generation of parameters is run from Cordform.
-    // KryoServerSerializationScheme is not accessible from nodeapi.
     private fun initialiseSerialization() {
         _contextSerializationEnv.set(SerializationEnvironmentImpl(
                 SerializationFactoryImpl().apply {
diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt
index b57c177d61..04744c9376 100644
--- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt
+++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt
@@ -1,141 +1,289 @@
 package net.corda.nodeapi.internal.network
 
-import net.corda.core.contracts.ContractClassName
-import net.corda.core.crypto.SecureHash
-import net.corda.core.node.services.AttachmentId
-import net.corda.nodeapi.internal.ContractsJar
-import net.corda.testing.common.internal.testNetworkParameters
+import com.typesafe.config.ConfigFactory
+import net.corda.cordform.CordformNode.NODE_INFO_DIRECTORY
+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.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.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.internal.createNodeInfoAndSigned
 import org.assertj.core.api.Assertions.assertThat
-import org.assertj.core.api.Assertions.assertThatIllegalArgumentException
+import org.assertj.core.api.Assertions.assertThatThrownBy
+import org.junit.After
+import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import java.nio.file.Path
+import kotlin.streams.toList
 
 class NetworkBootstrapperTest {
-    @Test
-    fun `no jars against empty whitelist`() {
-        val whitelist = generateWhitelist(emptyMap(), emptyList(), emptyList())
-        assertThat(whitelist).isEmpty()
-    }
+    @Rule
+    @JvmField
+    val tempFolder = TemporaryFolder()
 
-    @Test
-    fun `no jars against single whitelist`() {
-        val existingWhitelist = mapOf("class1" to listOf(SecureHash.randomSHA256()))
-        val newWhitelist = generateWhitelist(existingWhitelist, emptyList(), emptyList())
-        assertThat(newWhitelist).isEqualTo(existingWhitelist)
-    }
+    @Rule
+    @JvmField
+    val testSerialization = SerializationEnvironmentRule()
 
-    @Test
-    fun `empty jar against empty whitelist`() {
-        val whitelist = generateWhitelist(emptyMap(), emptyList(), listOf(TestContractsJar(contractClassNames = emptyList())))
-        assertThat(whitelist).isEmpty()
-    }
+    private val fakeEmbeddedCordaJar = fakeFileBytes()
 
-    @Test
-    fun `empty jar against single whitelist`() {
-        val existingWhitelist = mapOf("class1" to listOf(SecureHash.randomSHA256()))
-        val newWhitelist = generateWhitelist(existingWhitelist, emptyList(), listOf(TestContractsJar(contractClassNames = emptyList())))
-        assertThat(newWhitelist).isEqualTo(existingWhitelist)
-    }
+    private val contractsJars = HashMap<Path, TestContractsJar>()
 
-    @Test
-    fun `jar with single contract against empty whitelist`() {
-        val jar = TestContractsJar(contractClassNames = listOf("class1"))
-        val whitelist = generateWhitelist(emptyMap(), emptyList(), listOf(jar))
-        assertThat(whitelist).isEqualTo(mapOf(
-                "class1" to listOf(jar.hash)
-        ))
-    }
+    private val bootstrapper = NetworkBootstrapper(
+            initSerEnv = false,
+            embeddedCordaJar = fakeEmbeddedCordaJar::inputStream,
+            nodeInfosGenerator = { nodeDirs ->
+                nodeDirs.map { nodeDir ->
+                    val name = nodeDir.fakeNodeConfig.myLegalName
+                    val file = nodeDir / "$NODE_INFO_FILE_NAME_PREFIX${name.serialize().hash}"
+                    if (!file.exists()) {
+                        createNodeInfoAndSigned(name).signed.serialize().open().copyTo(file)
+                    }
+                    file
+                }
+            },
+            contractsJarConverter = { contractsJars[it]!! }
+    )
 
-    @Test
-    fun `single contract jar against single whitelist of different contract`() {
-        val class1JarHash = SecureHash.randomSHA256()
-        val existingWhitelist = mapOf("class1" to listOf(class1JarHash))
-        val jar = TestContractsJar(contractClassNames = listOf("class2"))
-        val whitelist = generateWhitelist(existingWhitelist, emptyList(), listOf(jar))
-        assertThat(whitelist).isEqualTo(mapOf(
-                "class1" to listOf(class1JarHash),
-                "class2" to listOf(jar.hash)
-        ))
-    }
+    private val aliceConfig = FakeNodeConfig(ALICE_NAME)
+    private val bobConfig = FakeNodeConfig(BOB_NAME)
+    private val notaryConfig = FakeNodeConfig(DUMMY_NOTARY_NAME, NotaryConfig(validating = true))
 
-    @Test
-    fun `same jar with single contract`() {
-        val jarHash = SecureHash.randomSHA256()
-        val existingWhitelist = mapOf("class1" to listOf(jarHash))
-        val jar = TestContractsJar(hash = jarHash, contractClassNames = listOf("class1"))
-        val newWhitelist = generateWhitelist(existingWhitelist, emptyList(), listOf(jar))
-        assertThat(newWhitelist).isEqualTo(existingWhitelist)
-    }
+    private var providedCordaJar: ByteArray? = null
+    private val configFiles = HashMap<Path, String>()
 
-    @Test
-    fun `jar with updated contract`() {
-        val previousJarHash = SecureHash.randomSHA256()
-        val existingWhitelist = mapOf("class1" to listOf(previousJarHash))
-        val newContractsJar = TestContractsJar(contractClassNames = listOf("class1"))
-        val newWhitelist = generateWhitelist(existingWhitelist, emptyList(), listOf(newContractsJar))
-        assertThat(newWhitelist).isEqualTo(mapOf(
-                "class1" to listOf(previousJarHash, newContractsJar.hash)
-        ))
-    }
-
-    @Test
-    fun `jar with one existing contract and one new one`() {
-        val previousJarHash = SecureHash.randomSHA256()
-        val existingWhitelist = mapOf("class1" to listOf(previousJarHash))
-        val newContractsJar = TestContractsJar(contractClassNames = listOf("class1", "class2"))
-        val newWhitelist = generateWhitelist(existingWhitelist, emptyList(), listOf(newContractsJar))
-        assertThat(newWhitelist).isEqualTo(mapOf(
-                "class1" to listOf(previousJarHash, newContractsJar.hash),
-                "class2" to listOf(newContractsJar.hash)
-        ))
-    }
-
-    @Test
-    fun `two versions of the same contract`() {
-        val version1Jar = TestContractsJar(contractClassNames = listOf("class1"))
-        val version2Jar = TestContractsJar(contractClassNames = listOf("class1"))
-        val newWhitelist = generateWhitelist(emptyMap(), emptyList(), listOf(version1Jar, version2Jar))
-        assertThat(newWhitelist).isEqualTo(mapOf(
-                "class1" to listOf(version1Jar.hash, version2Jar.hash)
-        ))
-    }
-
-    @Test
-    fun `jar with single new contract that's excluded`() {
-        val jar = TestContractsJar(contractClassNames = listOf("class1"))
-        val whitelist = generateWhitelist(emptyMap(), listOf("class1"), listOf(jar))
-        assertThat(whitelist).isEmpty()
-    }
-
-    @Test
-    fun `jar with two new contracts, one of which is excluded`() {
-        val jar = TestContractsJar(contractClassNames = listOf("class1", "class2"))
-        val whitelist = generateWhitelist(emptyMap(), listOf("class1"), listOf(jar))
-        assertThat(whitelist).isEqualTo(mapOf(
-                "class2" to listOf(jar.hash)
-        ))
-    }
-
-    @Test
-    fun `jar with updated contract but it's excluded`() {
-        val existingWhitelist = mapOf("class1" to listOf(SecureHash.randomSHA256()))
-        val jar = TestContractsJar(contractClassNames = listOf("class1"))
-        assertThatIllegalArgumentException().isThrownBy {
-            generateWhitelist(existingWhitelist, listOf("class1"), listOf(jar))
+    @After
+    fun `check config files are preserved`() {
+        configFiles.forEach { file, text ->
+            assertThat(file).hasContent(text)
         }
     }
 
-    private fun generateWhitelist(existingWhitelist: Map<String, List<AttachmentId>>,
-                                  excludeContracts: List<ContractClassName>,
-                                  contractJars: List<TestContractsJar>): Map<String, List<AttachmentId>> {
-        return generateWhitelist(
-                testNetworkParameters(whitelistedContractImplementations = existingWhitelist),
-                excludeContracts,
-                contractJars
-        )
+    @After
+    fun `check provided corda jar is preserved`() {
+        if (providedCordaJar == null) {
+            // Make sure we clean up if we used the embedded jar
+            assertThat(rootDir / "corda.jar").doesNotExist()
+        } else {
+            // Make sure we don't delete it if it was provided by the user
+            assertThat(rootDir / "corda.jar").hasBinaryContent(providedCordaJar)
+        }
     }
 
-    data class TestContractsJar(override val hash: SecureHash = SecureHash.randomSHA256(),
-                                private val contractClassNames: List<ContractClassName>) : ContractsJar {
-        override fun scan(): List<ContractClassName> = contractClassNames
+    @Test
+    fun `empty dir`() {
+        assertThatThrownBy {
+            bootstrap()
+        }.hasMessage("No nodes found")
     }
+
+    @Test
+    fun `single node conf file`() {
+        createNodeConfFile("node1", bobConfig)
+        bootstrap()
+        val networkParameters = assertBootstrappedNetwork(fakeEmbeddedCordaJar, "node1" to bobConfig)
+        networkParameters.run {
+            assertThat(epoch).isEqualTo(1)
+            assertThat(notaries).isEmpty()
+            assertThat(whitelistedContractImplementations).isEmpty()
+        }
+    }
+
+    @Test
+    fun `node conf file and corda jar`() {
+        createNodeConfFile("node1", bobConfig)
+        val fakeCordaJar = fakeFileBytes(rootDir / "corda.jar")
+        bootstrap()
+        assertBootstrappedNetwork(fakeCordaJar, "node1" to bobConfig)
+    }
+
+    @Test
+    fun `single node directory with just node conf file`() {
+        createNodeDir("bob", bobConfig)
+        bootstrap()
+        assertBootstrappedNetwork(fakeEmbeddedCordaJar, "bob" to bobConfig)
+    }
+
+    @Test
+    fun `single node directory with node conf file and corda jar`() {
+        val nodeDir = createNodeDir("bob", bobConfig)
+        val fakeCordaJar = fakeFileBytes(nodeDir / "corda.jar")
+        bootstrap()
+        assertBootstrappedNetwork(fakeCordaJar, "bob" to bobConfig)
+    }
+
+    @Test
+    fun `single node directory with just corda jar`() {
+        val nodeCordaJar = (rootDir / "alice").createDirectories() / "corda.jar"
+        val fakeCordaJar = fakeFileBytes(nodeCordaJar)
+        assertThatThrownBy {
+            bootstrap()
+        }.hasMessageStartingWith("Missing node.conf in node directory alice")
+        assertThat(nodeCordaJar).hasBinaryContent(fakeCordaJar)  // Make sure the corda.jar is left untouched
+    }
+
+    @Test
+    fun `two node conf files, one of which is a notary`() {
+        createNodeConfFile("alice", aliceConfig)
+        createNodeConfFile("notary", notaryConfig)
+        bootstrap()
+        val networkParameters = assertBootstrappedNetwork(fakeEmbeddedCordaJar, "alice" to aliceConfig, "notary" to notaryConfig)
+        networkParameters.assertContainsNotary("notary")
+    }
+
+    @Test
+    fun `two node conf files with the same legal name`() {
+        createNodeConfFile("node1", aliceConfig)
+        createNodeConfFile("node2", aliceConfig)
+        assertThatThrownBy {
+            bootstrap()
+        }.hasMessageContaining("Nodes must have unique legal names")
+    }
+
+    @Test
+    fun `one node directory and one node conf file`() {
+        createNodeConfFile("alice", aliceConfig)
+        createNodeDir("bob", bobConfig)
+        bootstrap()
+        assertBootstrappedNetwork(fakeEmbeddedCordaJar, "alice" to aliceConfig, "bob" to bobConfig)
+    }
+
+    @Test
+    fun `node conf file and CorDapp jar`() {
+        createNodeConfFile("alice", aliceConfig)
+        val cordappBytes = createFakeCordappJar("sample-app", listOf("contract.class"))
+        bootstrap()
+        val networkParameters = assertBootstrappedNetwork(fakeEmbeddedCordaJar, "alice" to aliceConfig)
+        assertThat(rootDir / "alice" / "cordapps" / "sample-app.jar").hasBinaryContent(cordappBytes)
+        assertThat(networkParameters.whitelistedContractImplementations).isEqualTo(mapOf(
+                "contract.class" to listOf(cordappBytes.sha256())
+        ))
+    }
+
+    @Test
+    fun `no copy CorDapps`() {
+        createNodeConfFile("alice", aliceConfig)
+        val cordappBytes = createFakeCordappJar("sample-app", listOf("contract.class"))
+        bootstrap(copyCordapps = false)
+        val networkParameters = assertBootstrappedNetwork(fakeEmbeddedCordaJar, "alice" to aliceConfig)
+        assertThat(rootDir / "alice" / "cordapps" / "sample-app.jar").doesNotExist()
+        assertThat(networkParameters.whitelistedContractImplementations).isEqualTo(mapOf(
+                "contract.class" to listOf(cordappBytes.sha256())
+        ))
+    }
+
+    @Test
+    fun `add node to existing network`() {
+        createNodeConfFile("alice", aliceConfig)
+        bootstrap()
+        val networkParameters1 = (rootDir / "alice").networkParameters
+        createNodeConfFile("bob", bobConfig)
+        bootstrap()
+        val networkParameters2 = assertBootstrappedNetwork(fakeEmbeddedCordaJar, "alice" to aliceConfig, "bob" to bobConfig)
+        assertThat(networkParameters1).isEqualTo(networkParameters2)
+    }
+
+    @Test
+    fun `add notary to existing network`() {
+        createNodeConfFile("alice", aliceConfig)
+        bootstrap()
+        createNodeConfFile("notary", notaryConfig)
+        bootstrap()
+        val networkParameters = assertBootstrappedNetwork(fakeEmbeddedCordaJar, "alice" to aliceConfig, "notary" to notaryConfig)
+        networkParameters.assertContainsNotary("notary")
+        assertThat(networkParameters.epoch).isEqualTo(2)
+    }
+
+    private val rootDir get() = tempFolder.root.toPath()
+
+    private fun fakeFileBytes(writeToFile: Path? = null): ByteArray {
+        val bytes = secureRandomBytes(128)
+        writeToFile?.write(bytes)
+        return bytes
+    }
+
+    private fun bootstrap(copyCordapps: Boolean = true) {
+        providedCordaJar = (rootDir / "corda.jar").let { if (it.exists()) it.readAll() else null }
+        bootstrapper.bootstrap(rootDir, copyCordapps)
+    }
+
+    private fun createNodeConfFile(nodeDirName: String, config: FakeNodeConfig) {
+        writeNodeConfFile(rootDir / "${nodeDirName}_node.conf", config)
+    }
+
+    private fun createNodeDir(nodeDirName: String, config: FakeNodeConfig): Path {
+        val nodeDir = (rootDir / nodeDirName).createDirectories()
+        writeNodeConfFile(nodeDir / "node.conf", config)
+        return nodeDir
+    }
+
+    private fun writeNodeConfFile(file: Path, config: FakeNodeConfig) {
+        val configText = config.toConfig().root().render()
+        file.writeText(configText)
+        configFiles[file] = configText
+    }
+
+    private fun createFakeCordappJar(cordappName: String, contractClassNames: List<String>): ByteArray {
+        val cordappJarFile = rootDir / "$cordappName.jar"
+        val cordappBytes = fakeFileBytes(cordappJarFile)
+        contractsJars[cordappJarFile] = TestContractsJar(cordappBytes.sha256(), contractClassNames)
+        return cordappBytes
+    }
+
+    private val Path.networkParameters: NetworkParameters get() {
+        return (this / NETWORK_PARAMS_FILE_NAME).readObject<SignedNetworkParameters>().verifiedNetworkMapCert(DEV_ROOT_CA.certificate)
+    }
+
+    private val Path.nodeInfoFile: Path get() {
+        return list { it.filter { it.fileName.toString().startsWith(NODE_INFO_FILE_NAME_PREFIX) }.toList() }.single()
+    }
+
+    private val Path.nodeInfo: NodeInfo get() = nodeInfoFile.readObject<SignedNodeInfo>().verified()
+
+    private val Path.fakeNodeConfig: FakeNodeConfig get() {
+        return ConfigFactory.parseFile((this / "node.conf").toFile()).parseAs(FakeNodeConfig::class)
+    }
+
+    private fun assertBootstrappedNetwork(cordaJar: ByteArray, vararg nodes: Pair<String, FakeNodeConfig>): NetworkParameters {
+        val networkParameters = (rootDir / nodes[0].first).networkParameters
+        val allNodeInfoFiles = nodes.map { (rootDir / it.first).nodeInfoFile }.associateBy({ it }, { it.readAll() })
+
+        for ((nodeDirName, config) in nodes) {
+            val nodeDir = rootDir / nodeDirName
+            assertThat(nodeDir / "corda.jar").hasBinaryContent(cordaJar)
+            assertThat(nodeDir.fakeNodeConfig).isEqualTo(config)
+            assertThat(nodeDir.networkParameters).isEqualTo(networkParameters)
+            // Make sure all the nodes have all of each others' node-info files
+            allNodeInfoFiles.forEach { nodeInfoFile, bytes ->
+                assertThat(nodeDir / NODE_INFO_DIRECTORY / nodeInfoFile.fileName.toString()).hasBinaryContent(bytes)
+            }
+        }
+
+        return networkParameters
+    }
+
+    private fun NetworkParameters.assertContainsNotary(dirName: String) {
+        val notaryParty = (rootDir / dirName).nodeInfo.legalIdentities.single()
+        assertThat(notaries).hasSize(1)
+        notaries[0].run {
+            assertThat(validating).isTrue()
+            assertThat(identity.name).isEqualTo(notaryParty.name)
+            assertThat(identity.owningKey).isEqualTo(notaryParty.owningKey)
+        }
+    }
+
+    data class FakeNodeConfig(val myLegalName: CordaX500Name, val notary: NotaryConfig? = null)
 }
diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/TestContractsJar.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/TestContractsJar.kt
new file mode 100644
index 0000000000..ea1a4ffc88
--- /dev/null
+++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/TestContractsJar.kt
@@ -0,0 +1,10 @@
+package net.corda.nodeapi.internal.network
+
+import net.corda.core.contracts.ContractClassName
+import net.corda.core.crypto.SecureHash
+import net.corda.nodeapi.internal.ContractsJar
+
+data class TestContractsJar(override val hash: SecureHash = SecureHash.randomSHA256(),
+                            private val contractClassNames: List<ContractClassName>) : ContractsJar {
+    override fun scan(): List<ContractClassName> = contractClassNames
+}
diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/WhitelistGeneratorTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/WhitelistGeneratorTest.kt
index 67f4bb9641..4bf6960c85 100644
--- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/WhitelistGeneratorTest.kt
+++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/WhitelistGeneratorTest.kt
@@ -1,41 +1,135 @@
 package net.corda.nodeapi.internal.network
 
-import com.nhaarman.mockito_kotlin.mock
-import com.nhaarman.mockito_kotlin.verify
+import net.corda.core.contracts.ContractClassName
 import net.corda.core.crypto.SecureHash
-import net.corda.nodeapi.internal.ContractsJar
+import net.corda.core.node.services.AttachmentId
+import net.corda.testing.common.internal.testNetworkParameters
+import org.assertj.core.api.Assertions.assertThat
+import org.assertj.core.api.Assertions.assertThatIllegalArgumentException
 import org.junit.Test
-import kotlin.test.assertEquals
-import kotlin.test.assertTrue
 
 class WhitelistGeneratorTest {
+    @Test
+    fun `no jars against empty whitelist`() {
+        val whitelist = generateWhitelist(emptyMap(), emptyList(), emptyList())
+        assertThat(whitelist).isEmpty()
+    }
 
     @Test
-    fun `whitelist generator builds the correct whitelist map`() {
-        // given
-        val jars = (0..9).map {
-            val index = it
-            mock<ContractsJar> {
-                val secureHash = SecureHash.randomSHA256()
-                on { scan() }.then {
-                    listOf(index.toString())
-                }
-                on { hash }.then {
-                    secureHash
-                }
-            }
-        }
+    fun `no jars against single whitelist`() {
+        val existingWhitelist = mapOf("class1" to listOf(SecureHash.randomSHA256()))
+        val newWhitelist = generateWhitelist(existingWhitelist, emptyList(), emptyList())
+        assertThat(newWhitelist).isEqualTo(existingWhitelist)
+    }
 
-        // when
-        val result = generateWhitelist(null, emptyList(), jars)
+    @Test
+    fun `empty jar against empty whitelist`() {
+        val whitelist = generateWhitelist(emptyMap(), emptyList(), listOf(TestContractsJar(contractClassNames = emptyList())))
+        assertThat(whitelist).isEmpty()
+    }
 
-        // then
-        jars.forEachIndexed { index, item ->
-            verify(item).scan()
-            val attachmentIds = requireNotNull(result[index.toString()])
-            assertEquals(1, attachmentIds.size)
-            assertTrue { attachmentIds.contains(item.hash) }
+    @Test
+    fun `empty jar against single whitelist`() {
+        val existingWhitelist = mapOf("class1" to listOf(SecureHash.randomSHA256()))
+        val newWhitelist = generateWhitelist(existingWhitelist, emptyList(), listOf(TestContractsJar(contractClassNames = emptyList())))
+        assertThat(newWhitelist).isEqualTo(existingWhitelist)
+    }
+
+    @Test
+    fun `jar with single contract against empty whitelist`() {
+        val jar = TestContractsJar(contractClassNames = listOf("class1"))
+        val whitelist = generateWhitelist(emptyMap(), emptyList(), listOf(jar))
+        assertThat(whitelist).isEqualTo(mapOf(
+                "class1" to listOf(jar.hash)
+        ))
+    }
+
+    @Test
+    fun `single contract jar against single whitelist of different contract`() {
+        val class1JarHash = SecureHash.randomSHA256()
+        val existingWhitelist = mapOf("class1" to listOf(class1JarHash))
+        val jar = TestContractsJar(contractClassNames = listOf("class2"))
+        val whitelist = generateWhitelist(existingWhitelist, emptyList(), listOf(jar))
+        assertThat(whitelist).isEqualTo(mapOf(
+                "class1" to listOf(class1JarHash),
+                "class2" to listOf(jar.hash)
+        ))
+    }
+
+    @Test
+    fun `same jar with single contract`() {
+        val jarHash = SecureHash.randomSHA256()
+        val existingWhitelist = mapOf("class1" to listOf(jarHash))
+        val jar = TestContractsJar(hash = jarHash, contractClassNames = listOf("class1"))
+        val newWhitelist = generateWhitelist(existingWhitelist, emptyList(), listOf(jar))
+        assertThat(newWhitelist).isEqualTo(existingWhitelist)
+    }
+
+    @Test
+    fun `jar with updated contract`() {
+        val previousJarHash = SecureHash.randomSHA256()
+        val existingWhitelist = mapOf("class1" to listOf(previousJarHash))
+        val newContractsJar = TestContractsJar(contractClassNames = listOf("class1"))
+        val newWhitelist = generateWhitelist(existingWhitelist, emptyList(), listOf(newContractsJar))
+        assertThat(newWhitelist).isEqualTo(mapOf(
+                "class1" to listOf(previousJarHash, newContractsJar.hash)
+        ))
+    }
+
+    @Test
+    fun `jar with one existing contract and one new one`() {
+        val previousJarHash = SecureHash.randomSHA256()
+        val existingWhitelist = mapOf("class1" to listOf(previousJarHash))
+        val newContractsJar = TestContractsJar(contractClassNames = listOf("class1", "class2"))
+        val newWhitelist = generateWhitelist(existingWhitelist, emptyList(), listOf(newContractsJar))
+        assertThat(newWhitelist).isEqualTo(mapOf(
+                "class1" to listOf(previousJarHash, newContractsJar.hash),
+                "class2" to listOf(newContractsJar.hash)
+        ))
+    }
+
+    @Test
+    fun `two versions of the same contract`() {
+        val version1Jar = TestContractsJar(contractClassNames = listOf("class1"))
+        val version2Jar = TestContractsJar(contractClassNames = listOf("class1"))
+        val newWhitelist = generateWhitelist(emptyMap(), emptyList(), listOf(version1Jar, version2Jar))
+        assertThat(newWhitelist).isEqualTo(mapOf(
+                "class1" to listOf(version1Jar.hash, version2Jar.hash)
+        ))
+    }
+
+    @Test
+    fun `jar with single new contract that's excluded`() {
+        val jar = TestContractsJar(contractClassNames = listOf("class1"))
+        val whitelist = generateWhitelist(emptyMap(), listOf("class1"), listOf(jar))
+        assertThat(whitelist).isEmpty()
+    }
+
+    @Test
+    fun `jar with two new contracts, one of which is excluded`() {
+        val jar = TestContractsJar(contractClassNames = listOf("class1", "class2"))
+        val whitelist = generateWhitelist(emptyMap(), listOf("class1"), listOf(jar))
+        assertThat(whitelist).isEqualTo(mapOf(
+                "class2" to listOf(jar.hash)
+        ))
+    }
+
+    @Test
+    fun `jar with updated contract but it's excluded`() {
+        val existingWhitelist = mapOf("class1" to listOf(SecureHash.randomSHA256()))
+        val jar = TestContractsJar(contractClassNames = listOf("class1"))
+        assertThatIllegalArgumentException().isThrownBy {
+            generateWhitelist(existingWhitelist, listOf("class1"), listOf(jar))
         }
     }
 
-}
\ No newline at end of file
+    private fun generateWhitelist(existingWhitelist: Map<String, List<AttachmentId>>,
+                                  excludeContracts: List<ContractClassName>,
+                                  contractJars: List<TestContractsJar>): Map<String, List<AttachmentId>> {
+        return generateWhitelist(
+                testNetworkParameters(whitelistedContractImplementations = existingWhitelist),
+                excludeContracts,
+                contractJars
+        )
+    }
+}
diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt
index 0d782bbfc3..9db888b4c9 100644
--- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt
+++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt
@@ -288,7 +288,7 @@ open class NodeStartup(val args: Array<String>) {
         logger.info("Revision: ${versionInfo.revision}")
         val info = ManagementFactory.getRuntimeMXBean()
         logger.info("PID: ${info.name.split("@").firstOrNull()}")  // TODO Java 9 has better support for this
-        logger.info("Main class: ${NodeConfiguration::class.java.protectionDomain.codeSource.location.toURI().path}")
+        logger.info("Main class: ${NodeConfiguration::class.java.location.toURI().path}")
         logger.info("CommandLine Args: ${info.inputArguments.joinToString(" ")}")
         logger.info("Application Args: ${args.joinToString(" ")}")
         logger.info("bootclasspath: ${info.bootClassPath}")
diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt
index 04af02b113..ce9f261a4f 100644
--- a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt
+++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt
@@ -315,7 +315,7 @@ class CordappLoader private constructor(private val cordappJarPaths: List<Restri
 
     private fun findPlugins(cordappJarPath: RestrictedURL): List<SerializationWhitelist> {
         return ServiceLoader.load(SerializationWhitelist::class.java, URLClassLoader(arrayOf(cordappJarPath.url), appClassLoader)).toList().filter {
-            it.javaClass.protectionDomain.codeSource.location == cordappJarPath.url && it.javaClass.name.startsWith(cordappJarPath.qualifiedNamePrefix)
+            it.javaClass.location == cordappJarPath.url && it.javaClass.name.startsWith(cordappJarPath.qualifiedNamePrefix)
         } + DefaultWhitelist // Always add the DefaultWhitelist to the whitelist for an app.
     }
 
diff --git a/node/src/main/kotlin/net/corda/node/serialization/amqp/AMQPServerSerializationScheme.kt b/node/src/main/kotlin/net/corda/node/serialization/amqp/AMQPServerSerializationScheme.kt
index 70bcaaf0da..2f28d692a2 100644
--- a/node/src/main/kotlin/net/corda/node/serialization/amqp/AMQPServerSerializationScheme.kt
+++ b/node/src/main/kotlin/net/corda/node/serialization/amqp/AMQPServerSerializationScheme.kt
@@ -26,15 +26,13 @@ class AMQPServerSerializationScheme(
         throw UnsupportedOperationException()
     }
 
-    override fun rpcServerSerializerFactory(context: SerializationContext) =
-        SerializerFactory(
-                context.whitelist,
-                context.deserializationClassLoader
-        ).apply {
+    override fun rpcServerSerializerFactory(context: SerializationContext): SerializerFactory {
+        return SerializerFactory(context.whitelist, context.deserializationClassLoader, context.lenientCarpenterEnabled).apply {
             register(RpcServerObservableSerializer())
             register(RpcServerCordaFutureSerializer(this))
             register(RxNotificationSerializer(this))
         }
+    }
 
     override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean {
         return canDeserializeVersion(magic) &&
diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt
index ed47f9c67d..0737672e7c 100644
--- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt
+++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt
@@ -446,7 +446,7 @@ val Class<out FlowLogic<*>>.flowVersionAndInitiatingClass: Pair<Int, Class<out F
 
 val Class<out FlowLogic<*>>.appName: String
     get() {
-        val jarFile = protectionDomain.codeSource.location.toPath()
+        val jarFile = location.toPath()
         return if (jarFile.isRegularFile() && jarFile.toString().endsWith(".jar")) {
             jarFile.fileName.toString().removeSuffix(".jar")
         } else {
diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/SerializationScheme.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/SerializationScheme.kt
index 5c3ab76fc4..0176dd63ca 100644
--- a/serialization/src/main/kotlin/net/corda/serialization/internal/SerializationScheme.kt
+++ b/serialization/src/main/kotlin/net/corda/serialization/internal/SerializationScheme.kt
@@ -40,7 +40,8 @@ data class SerializationContextImpl @JvmOverloads constructor(override val prefe
                                                               override val objectReferencesEnabled: Boolean,
                                                               override val useCase: SerializationContext.UseCase,
                                                               override val encoding: SerializationEncoding?,
-                                                              override val encodingWhitelist: EncodingWhitelist = NullEncodingWhitelist) : SerializationContext {
+                                                              override val encodingWhitelist: EncodingWhitelist = NullEncodingWhitelist,
+                                                              override val lenientCarpenterEnabled: Boolean = false) : SerializationContext {
     private val builder = AttachmentsClassLoaderBuilder(properties, deserializationClassLoader)
 
     /**
@@ -62,6 +63,8 @@ data class SerializationContextImpl @JvmOverloads constructor(override val prefe
         return copy(objectReferencesEnabled = false)
     }
 
+    override fun withLenientCarpenter(): SerializationContext = copy(lenientCarpenterEnabled = true)
+
     override fun withClassLoader(classLoader: ClassLoader): SerializationContext {
         return copy(deserializationClassLoader = classLoader)
     }
diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializerFactories.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializerFactories.kt
index 9876855d3d..cf47262ab8 100644
--- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializerFactories.kt
+++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializerFactories.kt
@@ -15,6 +15,7 @@ import net.corda.core.serialization.SerializationContext
 fun createSerializerFactoryFactory(): SerializerFactoryFactory = SerializerFactoryFactoryImpl()
 
 open class SerializerFactoryFactoryImpl : SerializerFactoryFactory {
-    override fun make(context: SerializationContext) =
-            SerializerFactory(context.whitelist, context.deserializationClassLoader)
+    override fun make(context: SerializationContext): SerializerFactory {
+        return SerializerFactory(context.whitelist, context.deserializationClassLoader, context.lenientCarpenterEnabled)
+    }
 }
diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ObjectSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ObjectSerializer.kt
index 4a0f18f38d..002ea7488d 100644
--- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ObjectSerializer.kt
+++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ObjectSerializer.kt
@@ -17,6 +17,8 @@ import net.corda.serialization.internal.amqp.SerializerFactory.Companion.nameFor
 import org.apache.qpid.proton.amqp.Symbol
 import org.apache.qpid.proton.codec.Data
 import java.io.NotSerializableException
+import java.lang.reflect.Constructor
+import java.lang.reflect.InvocationTargetException
 import java.lang.reflect.Type
 import kotlin.reflect.jvm.javaConstructor
 
@@ -39,8 +41,7 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS
 
     private val typeName = nameForType(clazz)
 
-    override val typeDescriptor = Symbol.valueOf(
-            "$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}")!!
+    override val typeDescriptor: Symbol = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}")
 
     // We restrict to only those annotated or whitelisted
     private val interfaces = interfacesForSerialization(clazz, factory)
@@ -129,7 +130,7 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS
             context: SerializationContext): Any = ifThrowsAppend({ clazz.typeName }) {
         logger.trace { "Calling setter based construction for ${clazz.typeName}" }
 
-        val instance: Any = javaConstructor?.newInstance() ?: throw NotSerializableException(
+        val instance: Any = javaConstructor?.newInstanceUnwrapped() ?: throw NotSerializableException(
                 "Failed to instantiate instance of object $clazz")
 
         // read the properties out of the serialised form, since we're invoking the setters the order we
@@ -163,7 +164,15 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS
                     + "serialized properties.")
         }
 
-        return javaConstructor?.newInstance(*properties.toTypedArray())
+        return javaConstructor?.newInstanceUnwrapped(*properties.toTypedArray())
                 ?: throw NotSerializableException("Attempt to deserialize an interface: $clazz. Serialized form is invalid.")
     }
+
+    private fun <T> Constructor<T>.newInstanceUnwrapped(vararg args: Any?): T {
+        try {
+            return newInstance(*args)
+        } catch (e: InvocationTargetException) {
+            throw e.cause!!
+        }
+    }
 }
\ No newline at end of file
diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt
index 2ed43e7667..7331bb29d4 100644
--- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt
+++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt
@@ -18,6 +18,7 @@ import net.corda.core.StubOutForDJVM
 import net.corda.core.internal.kotlinObjectInstance
 import net.corda.core.internal.uncheckedCast
 import net.corda.core.serialization.ClassWhitelist
+import net.corda.core.utilities.contextLogger
 import net.corda.core.utilities.debug
 import net.corda.core.utilities.loggerFor
 import net.corda.core.utilities.trace
@@ -63,40 +64,43 @@ open class SerializerFactory(
         private val serializersByType: MutableMap<Type, AMQPSerializer<Any>>,
         val serializersByDescriptor: MutableMap<Any, AMQPSerializer<Any>>,
         private val customSerializers: MutableList<SerializerFor>,
-        val transformsCache: MutableMap<String, EnumMap<TransformTypes, MutableList<Transform>>>) {
+        val transformsCache: MutableMap<String, EnumMap<TransformTypes, MutableList<Transform>>>
+) {
     @DeleteForDJVM
     constructor(whitelist: ClassWhitelist,
                 classCarpenter: ClassCarpenter,
                 evolutionSerializerGetter: EvolutionSerializerGetterBase = EvolutionSerializerGetter(),
                 fingerPrinter: FingerPrinter = SerializerFingerPrinter()
-    ) : this(whitelist, classCarpenter, evolutionSerializerGetter, fingerPrinter,
-             serializersByType = ConcurrentHashMap(),
-             serializersByDescriptor = ConcurrentHashMap(),
-             customSerializers = CopyOnWriteArrayList(),
-             transformsCache = ConcurrentHashMap())
+    ) : this(
+            whitelist,
+            classCarpenter,
+            evolutionSerializerGetter,
+            fingerPrinter,
+            ConcurrentHashMap(),
+            ConcurrentHashMap(),
+            CopyOnWriteArrayList(),
+            ConcurrentHashMap()
+    )
 
     @DeleteForDJVM
     constructor(whitelist: ClassWhitelist,
                 classLoader: ClassLoader,
+                lenientCarpenter: Boolean = false,
                 evolutionSerializerGetter: EvolutionSerializerGetterBase = EvolutionSerializerGetter(),
                 fingerPrinter: FingerPrinter = SerializerFingerPrinter()
-    ) : this(whitelist, ClassCarpenterImpl(classLoader, whitelist), evolutionSerializerGetter, fingerPrinter,
-             serializersByType = ConcurrentHashMap(),
-             serializersByDescriptor = ConcurrentHashMap(),
-             customSerializers = CopyOnWriteArrayList(),
-             transformsCache = ConcurrentHashMap())
+    ) : this(whitelist, ClassCarpenterImpl(classLoader, whitelist, lenientCarpenter), evolutionSerializerGetter, fingerPrinter)
 
     init {
         fingerPrinter.setOwner(this)
     }
 
-    val classloader: ClassLoader
-        get() = classCarpenter.classloader
+    val classloader: ClassLoader get() = classCarpenter.classloader
 
-    private fun getEvolutionSerializer(typeNotation: TypeNotation, newSerializer: AMQPSerializer<Any>,
-                                       schemas: SerializationSchemas) = evolutionSerializerGetter.getEvolutionSerializer(this, typeNotation, newSerializer, schemas)
-
-    private val logger = loggerFor<SerializerFactory>()
+    private fun getEvolutionSerializer(typeNotation: TypeNotation,
+                                       newSerializer: AMQPSerializer<Any>,
+                                       schemas: SerializationSchemas): AMQPSerializer<Any> {
+        return evolutionSerializerGetter.getEvolutionSerializer(this, typeNotation, newSerializer, schemas)
+    }
 
     /**
      * Look up, and manufacture if necessary, a serializer for the given type.
@@ -390,6 +394,8 @@ open class SerializerFactory(
     }
 
     companion object {
+        private val logger = contextLogger()
+
         fun isPrimitive(type: Type): Boolean = primitiveTypeName(type) != null
 
         fun primitiveTypeName(type: Type): String? {
@@ -479,4 +485,3 @@ open class SerializerFactory(
         override fun toString(): String = "?"
     }
 }
-
diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/AMQPSchemaExtensions.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/AMQPSchemaExtensions.kt
index 3ae9ac28e8..e01da083c3 100644
--- a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/AMQPSchemaExtensions.kt
+++ b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/AMQPSchemaExtensions.kt
@@ -73,7 +73,7 @@ fun CompositeType.carpenterSchema(classloader: ClassLoader,
         }
 
         try {
-            providesList.add(classloader.loadClass(it))
+            providesList.add(classloader.loadClass(it.stripGenerics()))
         } catch (e: ClassNotFoundException) {
             carpenterSchemas.addDepPair(this, name, it)
             isCreatable = false
diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenter.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenter.kt
index b6f1b06078..efcced603c 100644
--- a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenter.kt
+++ b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenter.kt
@@ -16,6 +16,8 @@ import net.corda.core.DeleteForDJVM
 import net.corda.core.KeepForDJVM
 import net.corda.core.serialization.ClassWhitelist
 import net.corda.core.serialization.CordaSerializable
+import net.corda.core.utilities.contextLogger
+import net.corda.core.utilities.debug
 import org.objectweb.asm.ClassWriter
 import org.objectweb.asm.MethodVisitor
 import org.objectweb.asm.Opcodes.*
@@ -112,8 +114,9 @@ interface ClassCarpenter {
  * Equals/hashCode methods are not yet supported.
  */
 @DeleteForDJVM
-class ClassCarpenterImpl(cl: ClassLoader, override val whitelist: ClassWhitelist) : ClassCarpenter {
-    constructor(whitelist: ClassWhitelist) : this(Thread.currentThread().contextClassLoader, whitelist)
+class ClassCarpenterImpl(cl: ClassLoader = Thread.currentThread().contextClassLoader,
+                         override val whitelist: ClassWhitelist,
+                         private val lenient: Boolean = false) : ClassCarpenter {
 
     // TODO: Generics.
     // TODO: Sandbox the generated code when a security manager is in use.
@@ -449,25 +452,36 @@ class ClassCarpenterImpl(cl: ClassLoader, override val whitelist: ClassWhitelist
         // actually called, which is a bit too dynamic for my tastes.
         val allFields = schema.fieldsIncludingSuperclasses()
         for (itf in schema.interfaces) {
-            itf.methods.forEach {
-                val fieldNameFromItf = when {
-                    it.name.startsWith("get") -> it.name.substring(3).decapitalize()
-                    else -> throw InterfaceMismatchNonGetterException(itf, it)
+            methodLoop@
+            for (method in itf.methods) {
+                val fieldNameFromItf = if (method.name.startsWith("get")) {
+                    method.name.substring(3).decapitalize()
+                } else if (lenient) {
+                    logger.debug { "Ignoring interface $method which is not a getter" }
+                    continue@methodLoop
+                } else {
+                    throw InterfaceMismatchNonGetterException(itf, method)
                 }
 
                 // If we're trying to carpent a class that prior to serialisation / deserialization
                 // was made by a carpenter then we can ignore this (it will implement a plain get
                 // method from SimpleFieldAccess).
-                if (fieldNameFromItf.isEmpty() && SimpleFieldAccess::class.java in schema.interfaces) return@forEach
+                if (fieldNameFromItf.isEmpty() && SimpleFieldAccess::class.java in schema.interfaces) continue@methodLoop
 
                 if ((schema is ClassSchema) and (fieldNameFromItf !in allFields)) {
-                    throw InterfaceMismatchMissingAMQPFieldException(itf, fieldNameFromItf)
+                    if (lenient) {
+                        logger.debug { "Ignoring interface $method which is not backed by an AMQP field" }
+                    } else {
+                        throw InterfaceMismatchMissingAMQPFieldException(itf, fieldNameFromItf)
+                    }
                 }
             }
         }
     }
 
     companion object {
+        private val logger = contextLogger()
+
         @JvmStatic
         @Suppress("UNUSED")
         fun getField(obj: Any, name: String): Any? = obj.javaClass.getMethod("get" + name.capitalize()).invoke(obj)
diff --git a/serialization/src/test/java/net/corda/serialization/internal/amqp/ErrorMessageTests.java b/serialization/src/test/java/net/corda/serialization/internal/amqp/ErrorMessageTests.java
index 2647692993..47e141e33a 100644
--- a/serialization/src/test/java/net/corda/serialization/internal/amqp/ErrorMessageTests.java
+++ b/serialization/src/test/java/net/corda/serialization/internal/amqp/ErrorMessageTests.java
@@ -18,6 +18,9 @@ import org.junit.Test;
 
 import java.io.NotSerializableException;
 
+import static net.corda.serialization.internal.amqp.testutils.AMQPTestUtilsKt.testDefaultFactory;
+import static org.assertj.core.api.Assertions.*;
+
 @Ignore("Current behaviour allows for the serialization of objects with private members, this will be disallowed at some point in the future")
 public class ErrorMessageTests {
     private String errMsg(String property, String testname) {
@@ -42,19 +45,10 @@ public class ErrorMessageTests {
 
     @Test
     public void testJavaConstructorAnnotations() {
-        EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter();
-        FingerPrinter fingerPrinter = new SerializerFingerPrinter();
-        SerializerFactory factory1 = new SerializerFactory(
-                AllWhitelist.INSTANCE,
-                ClassLoader.getSystemClassLoader(),
-                evolutionSerialiserGetter,
-                fingerPrinter);
+        SerializationOutput ser = new SerializationOutput(testDefaultFactory());
 
-        SerializationOutput ser = new SerializationOutput(factory1);
-
-        Assertions.assertThatThrownBy(() -> ser.serialize(new C(1), TestSerializationContext.testSerializationContext))
+        assertThatThrownBy(() -> ser.serialize(new C(1), TestSerializationContext.testSerializationContext))
                 .isInstanceOf(NotSerializableException.class)
                 .hasMessage(errMsg("a", getClass().getName()));
     }
-
 }
diff --git a/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaCustomSerializerTests.java b/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaCustomSerializerTests.java
index b6248d9c16..8c1bd4e665 100644
--- a/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaCustomSerializerTests.java
+++ b/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaCustomSerializerTests.java
@@ -1,7 +1,6 @@
 package net.corda.serialization.internal.amqp;
 
 import net.corda.core.serialization.SerializationCustomSerializer;
-import net.corda.serialization.internal.AllWhitelist;
 import net.corda.serialization.internal.amqp.testutils.TestSerializationContext;
 import org.junit.Test;
 
@@ -9,6 +8,8 @@ import java.io.NotSerializableException;
 import java.util.ArrayList;
 import java.util.List;
 
+import static net.corda.serialization.internal.amqp.testutils.AMQPTestUtilsKt.testDefaultFactory;
+
 public class JavaCustomSerializerTests {
     /**
      * The class lacks a public constructor that takes parameters it can associate
@@ -87,10 +88,8 @@ public class JavaCustomSerializerTests {
     }
 
     @Test
-    public void serializeExample() throws NotSerializableException, NoSuchFieldException, IllegalAccessException {
-        SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
-                new EvolutionSerializerGetter(),
-                new SerializerFingerPrinter());
+    public void serializeExample() throws NotSerializableException {
+        SerializerFactory factory = testDefaultFactory();
         SerializationOutput ser = new SerializationOutput(factory);
 
         List<Integer> l = new ArrayList<Integer>(2);
@@ -102,7 +101,5 @@ public class JavaCustomSerializerTests {
         factory.registerExternal(ccs);
 
         ser.serialize(e, TestSerializationContext.testSerializationContext);
-
-
     }
 }
diff --git a/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaGenericsTest.java b/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaGenericsTest.java
index 01a93e05c4..3736683a0c 100644
--- a/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaGenericsTest.java
+++ b/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaGenericsTest.java
@@ -11,11 +11,11 @@
 package net.corda.serialization.internal.amqp;
 
 import net.corda.core.serialization.SerializedBytes;
-import net.corda.serialization.internal.AllWhitelist;
 import net.corda.serialization.internal.amqp.testutils.TestSerializationContext;
 import org.junit.Test;
 import java.io.NotSerializableException;
 
+import static net.corda.serialization.internal.amqp.testutils.AMQPTestUtilsKt.testDefaultFactory;
 import static org.jgroups.util.Util.assertEquals;
 
 public class JavaGenericsTest {
@@ -37,11 +37,7 @@ public class JavaGenericsTest {
     public void basicGeneric() throws NotSerializableException {
         A a1 = new A(1);
 
-        SerializerFactory factory = new SerializerFactory(
-                AllWhitelist.INSTANCE,
-                ClassLoader.getSystemClassLoader(),
-                new EvolutionSerializerGetter(),
-                new SerializerFingerPrinter());
+        SerializerFactory factory = testDefaultFactory();
 
         SerializationOutput ser = new SerializationOutput(factory);
         SerializedBytes<?> bytes = ser.serialize(a1, TestSerializationContext.testSerializationContext);
@@ -53,13 +49,9 @@ public class JavaGenericsTest {
     }
 
     private SerializedBytes<?> forceWildcardSerialize(A<?> a) throws NotSerializableException {
-        SerializerFactory factory = new SerializerFactory(
-                AllWhitelist.INSTANCE,
-                ClassLoader.getSystemClassLoader(),
-                new EvolutionSerializerGetter(),
-                new SerializerFingerPrinter());
+        SerializerFactory factory = testDefaultFactory();
 
-       return (new SerializationOutput(factory)).serialize(a, TestSerializationContext.testSerializationContext);
+        return (new SerializationOutput(factory)).serialize(a, TestSerializationContext.testSerializationContext);
     }
 
     private SerializedBytes<?> forceWildcardSerializeFactory(
@@ -69,11 +61,7 @@ public class JavaGenericsTest {
     }
 
     private A<?> forceWildcardDeserialize(SerializedBytes<?> bytes) throws NotSerializableException {
-        SerializerFactory factory = new SerializerFactory(
-                AllWhitelist.INSTANCE,
-                ClassLoader.getSystemClassLoader(),
-                new EvolutionSerializerGetter(),
-                new SerializerFingerPrinter());
+        SerializerFactory factory = testDefaultFactory();
 
         DeserializationInput des = new DeserializationInput(factory);
         return des.deserialize(bytes, A.class, TestSerializationContext.testSerializationContext);
@@ -95,11 +83,7 @@ public class JavaGenericsTest {
 
     @Test
     public void forceWildcardSharedFactory() throws NotSerializableException {
-        SerializerFactory factory = new SerializerFactory(
-                AllWhitelist.INSTANCE,
-                ClassLoader.getSystemClassLoader(),
-                new EvolutionSerializerGetter(),
-                new SerializerFingerPrinter());
+        SerializerFactory factory = testDefaultFactory();
 
         SerializedBytes<?> bytes = forceWildcardSerializeFactory(new A(new Inner(29)), factory);
         Inner i = (Inner)forceWildcardDeserializeFactory(bytes, factory).getT();
diff --git a/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaNestedClassesTests.java b/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaNestedClassesTests.java
index 40fdc40cfe..603f6c1590 100644
--- a/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaNestedClassesTests.java
+++ b/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaNestedClassesTests.java
@@ -4,28 +4,25 @@ import com.google.common.collect.ImmutableList;
 import net.corda.core.contracts.ContractState;
 import net.corda.core.identity.AbstractParty;
 import net.corda.core.serialization.SerializedBytes;
-import net.corda.serialization.internal.AllWhitelist;
 import net.corda.serialization.internal.amqp.testutils.TestSerializationContext;
-import org.assertj.core.api.Assertions;
 import org.jetbrains.annotations.NotNull;
 import org.junit.Test;
 
 import java.io.NotSerializableException;
 import java.util.List;
 
+import static net.corda.serialization.internal.amqp.testutils.AMQPTestUtilsKt.testDefaultFactory;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
 class OuterClass1 {
     protected SerializationOutput ser;
     DeserializationInput desExisting;
     DeserializationInput desRegen;
 
     OuterClass1() {
-        SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
-                new EvolutionSerializerGetter(),
-                new SerializerFingerPrinter());
+        SerializerFactory factory1 = testDefaultFactory();
 
-        SerializerFactory factory2 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
-                new EvolutionSerializerGetter(),
-                new SerializerFingerPrinter());
+        SerializerFactory factory2 = testDefaultFactory();
 
         this.ser = new SerializationOutput(factory1);
         this.desExisting = new DeserializationInput(factory1);
@@ -59,13 +56,9 @@ class OuterClass2 {
     DeserializationInput desRegen;
 
     OuterClass2() {
-        SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
-                new EvolutionSerializerGetter(),
-                new SerializerFingerPrinter());
+        SerializerFactory factory1 = testDefaultFactory();
 
-        SerializerFactory factory2 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
-                new EvolutionSerializerGetter(),
-                new SerializerFingerPrinter());
+        SerializerFactory factory2 = testDefaultFactory();
 
         this.ser = new SerializationOutput(factory1);
         this.desExisting = new DeserializationInput(factory1);
@@ -104,9 +97,7 @@ abstract class AbstractClass2 {
     protected SerializationOutput ser;
 
     AbstractClass2() {
-        SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
-                new EvolutionSerializerGetter(),
-                new SerializerFingerPrinter());
+        SerializerFactory factory = testDefaultFactory();
 
         this.ser = new SerializationOutput(factory);
     }
@@ -134,9 +125,7 @@ abstract class AbstractClass3 {
 
 class Inherator5 extends AbstractClass3 {
     public void run() throws NotSerializableException {
-        SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
-                new EvolutionSerializerGetter(),
-                new SerializerFingerPrinter());
+        SerializerFactory factory = testDefaultFactory();
 
         SerializationOutput ser = new SerializationOutput(factory);
         ser.serialize(new DummyState(), TestSerializationContext.testSerializationContext);
@@ -154,9 +143,7 @@ class Inherator6 extends AbstractClass3 {
     }
 
     public void run() throws NotSerializableException {
-        SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
-                new EvolutionSerializerGetter(),
-                new SerializerFingerPrinter());
+        SerializerFactory factory = testDefaultFactory();
 
         SerializationOutput ser = new SerializationOutput(factory);
         ser.serialize(new Wrapper(new DummyState()), TestSerializationContext.testSerializationContext);
@@ -166,57 +153,57 @@ class Inherator6 extends AbstractClass3 {
 public class JavaNestedClassesTests {
     @Test
     public void publicNested() {
-        Assertions.assertThatThrownBy(() -> new OuterClass1().run()).isInstanceOf(
+        assertThatThrownBy(() -> new OuterClass1().run()).isInstanceOf(
                 NotSerializableException.class).hasMessageContaining(
                 "has synthetic fields and is likely a nested inner class");
     }
 
     @Test
     public void privateNested() {
-        Assertions.assertThatThrownBy(() -> new OuterClass2().run()).isInstanceOf(
+        assertThatThrownBy(() -> new OuterClass2().run()).isInstanceOf(
                 NotSerializableException.class).hasMessageContaining(
                 "has synthetic fields and is likely a nested inner class");
     }
 
     @Test
     public void publicNestedInherited() {
-        Assertions.assertThatThrownBy(() -> new Inherator1().run()).isInstanceOf(
+        assertThatThrownBy(() -> new Inherator1().run()).isInstanceOf(
                 NotSerializableException.class).hasMessageContaining(
                 "has synthetic fields and is likely a nested inner class");
 
-        Assertions.assertThatThrownBy(() -> new Inherator1().iRun()).isInstanceOf(
+        assertThatThrownBy(() -> new Inherator1().iRun()).isInstanceOf(
                 NotSerializableException.class).hasMessageContaining(
                 "has synthetic fields and is likely a nested inner class");
     }
 
     @Test
     public void protectedNestedInherited() {
-        Assertions.assertThatThrownBy(() -> new Inherator2().run()).isInstanceOf(
+        assertThatThrownBy(() -> new Inherator2().run()).isInstanceOf(
                 NotSerializableException.class).hasMessageContaining(
                 "has synthetic fields and is likely a nested inner class");
 
-        Assertions.assertThatThrownBy(() -> new Inherator2().iRun()).isInstanceOf(
+        assertThatThrownBy(() -> new Inherator2().iRun()).isInstanceOf(
                 NotSerializableException.class).hasMessageContaining(
                 "has synthetic fields and is likely a nested inner class");
     }
 
     @Test
     public void abstractNested() {
-        Assertions.assertThatThrownBy(() -> new Inherator4().run()).isInstanceOf(
+        assertThatThrownBy(() -> new Inherator4().run()).isInstanceOf(
                 NotSerializableException.class).hasMessageContaining(
                 "has synthetic fields and is likely a nested inner class");
     }
 
     @Test
     public void abstractNestedFactoryOnNested() {
-        Assertions.assertThatThrownBy(() -> new Inherator5().run()).isInstanceOf(
+        assertThatThrownBy(() -> new Inherator5().run()).isInstanceOf(
                 NotSerializableException.class).hasMessageContaining(
                 "has synthetic fields and is likely a nested inner class");
     }
 
     @Test
     public void abstractNestedFactoryOnNestedInWrapper() {
-        Assertions.assertThatThrownBy(() -> new Inherator6().run()).isInstanceOf(
+        assertThatThrownBy(() -> new Inherator6().run()).isInstanceOf(
                 NotSerializableException.class).hasMessageContaining(
                 "has synthetic fields and is likely a nested inner class");
     }
diff --git a/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaNestedInheritenceTests.java b/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaNestedInheritenceTests.java
index 039dacd688..5331b95096 100644
--- a/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaNestedInheritenceTests.java
+++ b/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaNestedInheritenceTests.java
@@ -3,15 +3,16 @@ package net.corda.serialization.internal.amqp;
 import com.google.common.collect.ImmutableList;
 import net.corda.core.contracts.ContractState;
 import net.corda.core.identity.AbstractParty;
-import net.corda.serialization.internal.AllWhitelist;
 import net.corda.serialization.internal.amqp.testutils.TestSerializationContext;
-import org.assertj.core.api.Assertions;
 import org.jetbrains.annotations.NotNull;
 import org.junit.Test;
 
 import java.io.NotSerializableException;
 import java.util.List;
 
+import static net.corda.serialization.internal.amqp.testutils.AMQPTestUtilsKt.testDefaultFactory;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
 abstract class JavaNestedInheritenceTestsBase {
     class DummyState implements ContractState {
         @Override @NotNull public List<AbstractParty> getParticipants() {
@@ -33,38 +34,32 @@ class TemplateWrapper<T> {
 public class JavaNestedInheritenceTests extends JavaNestedInheritenceTestsBase {
     @Test
     public void serializeIt() {
-        SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
-                new EvolutionSerializerGetter(),
-                new SerializerFingerPrinter());
+        SerializerFactory factory = testDefaultFactory();
 
         SerializationOutput ser = new SerializationOutput(factory);
 
-        Assertions.assertThatThrownBy(() -> ser.serialize(new DummyState(), TestSerializationContext.testSerializationContext)).isInstanceOf(
+        assertThatThrownBy(() -> ser.serialize(new DummyState(), TestSerializationContext.testSerializationContext)).isInstanceOf(
                 NotSerializableException.class).hasMessageContaining(
                 "has synthetic fields and is likely a nested inner class");
     }
 
     @Test
     public void serializeIt2() {
-        SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
-                new EvolutionSerializerGetter(),
-                new SerializerFingerPrinter());
+        SerializerFactory factory = testDefaultFactory();
 
         SerializationOutput ser = new SerializationOutput(factory);
-        Assertions.assertThatThrownBy(() -> ser.serialize(new Wrapper (new DummyState()), TestSerializationContext.testSerializationContext)).isInstanceOf(
+        assertThatThrownBy(() -> ser.serialize(new Wrapper (new DummyState()), TestSerializationContext.testSerializationContext)).isInstanceOf(
                 NotSerializableException.class).hasMessageContaining(
                 "has synthetic fields and is likely a nested inner class");
     }
 
     @Test
-    public void serializeIt3() throws NotSerializableException {
-        SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
-                new EvolutionSerializerGetter(),
-                new SerializerFingerPrinter());
+    public void serializeIt3() {
+        SerializerFactory factory1 = testDefaultFactory();
 
         SerializationOutput ser = new SerializationOutput(factory1);
 
-        Assertions.assertThatThrownBy(() -> ser.serialize(new TemplateWrapper<ContractState> (new DummyState()), TestSerializationContext.testSerializationContext)).isInstanceOf(
+        assertThatThrownBy(() -> ser.serialize(new TemplateWrapper<ContractState> (new DummyState()), TestSerializationContext.testSerializationContext)).isInstanceOf(
                 NotSerializableException.class).hasMessageContaining(
                 "has synthetic fields and is likely a nested inner class");
     }
diff --git a/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaPrivatePropertyTests.java b/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaPrivatePropertyTests.java
index 47864a7c7a..99e9ba2b7c 100644
--- a/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaPrivatePropertyTests.java
+++ b/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaPrivatePropertyTests.java
@@ -10,15 +10,16 @@
 
 package net.corda.serialization.internal.amqp;
 
-import net.corda.serialization.internal.AllWhitelist;
 import net.corda.serialization.internal.amqp.testutils.TestSerializationContext;
 import org.junit.Test;
-import static org.junit.Assert.*;
 
 import java.io.NotSerializableException;
 import java.lang.reflect.Field;
 import java.util.Map;
 
+import static net.corda.serialization.internal.amqp.testutils.AMQPTestUtilsKt.testDefaultFactory;
+import static org.junit.Assert.*;
+
 public class JavaPrivatePropertyTests {
     static class C {
         private String a;
@@ -86,10 +87,8 @@ public class JavaPrivatePropertyTests {
     }
 
     @Test
-    public void singlePrivateBooleanWithConstructor() throws NotSerializableException, NoSuchFieldException, IllegalAccessException {
-        SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
-                new EvolutionSerializerGetter(),
-                new SerializerFingerPrinter());
+    public void singlePrivateBooleanWithConstructor() throws NotSerializableException {
+        SerializerFactory factory = testDefaultFactory();
         SerializationOutput ser = new SerializationOutput(factory);
         DeserializationInput des = new DeserializationInput(factory);
 
@@ -99,10 +98,8 @@ public class JavaPrivatePropertyTests {
     }
 
     @Test
-    public void singlePrivateBooleanWithNoConstructor() throws NotSerializableException, NoSuchFieldException, IllegalAccessException {
-        SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
-                new EvolutionSerializerGetter(),
-                new SerializerFingerPrinter());
+    public void singlePrivateBooleanWithNoConstructor() throws NotSerializableException {
+        SerializerFactory factory = testDefaultFactory();
 
         SerializationOutput ser = new SerializationOutput(factory);
         DeserializationInput des = new DeserializationInput(factory);
@@ -114,10 +111,8 @@ public class JavaPrivatePropertyTests {
     }
 
     @Test
-    public void testCapitilsationOfIs() throws NotSerializableException, NoSuchFieldException, IllegalAccessException {
-        SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
-                new EvolutionSerializerGetter(),
-                new SerializerFingerPrinter());
+    public void testCapitilsationOfIs() throws NotSerializableException {
+        SerializerFactory factory = testDefaultFactory();
         SerializationOutput ser = new SerializationOutput(factory);
         DeserializationInput des = new DeserializationInput(factory);
 
@@ -130,10 +125,8 @@ public class JavaPrivatePropertyTests {
     }
 
     @Test
-    public void singlePrivateIntWithBoolean() throws NotSerializableException, NoSuchFieldException, IllegalAccessException {
-        SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
-                new EvolutionSerializerGetter(),
-                new SerializerFingerPrinter());
+    public void singlePrivateIntWithBoolean() throws NotSerializableException {
+        SerializerFactory factory = testDefaultFactory();
         SerializationOutput ser = new SerializationOutput(factory);
         DeserializationInput des = new DeserializationInput(factory);
 
@@ -146,9 +139,7 @@ public class JavaPrivatePropertyTests {
 
     @Test
     public void singlePrivateWithConstructor() throws NotSerializableException, NoSuchFieldException, IllegalAccessException {
-        SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
-                new EvolutionSerializerGetter(),
-                new SerializerFingerPrinter());
+        SerializerFactory factory = testDefaultFactory();
 
         SerializationOutput ser = new SerializationOutput(factory);
         DeserializationInput des = new DeserializationInput(factory);
@@ -176,10 +167,7 @@ public class JavaPrivatePropertyTests {
     @Test
     public void singlePrivateWithConstructorAndGetter()
             throws NotSerializableException, NoSuchFieldException, IllegalAccessException {
-        SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE,
-                ClassLoader.getSystemClassLoader(),
-                new EvolutionSerializerGetter(),
-                new SerializerFingerPrinter());
+        SerializerFactory factory = testDefaultFactory();
 
         SerializationOutput ser = new SerializationOutput(factory);
         DeserializationInput des = new DeserializationInput(factory);
diff --git a/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaSerialiseEnumTests.java b/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaSerialiseEnumTests.java
index c06100504b..8adcb878f4 100644
--- a/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaSerialiseEnumTests.java
+++ b/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaSerialiseEnumTests.java
@@ -10,14 +10,14 @@
 
 package net.corda.serialization.internal.amqp;
 
+import net.corda.core.serialization.SerializedBytes;
 import net.corda.serialization.internal.amqp.testutils.TestSerializationContext;
 import org.junit.Test;
 
-import net.corda.serialization.internal.AllWhitelist;
-import net.corda.core.serialization.SerializedBytes;
-
 import java.io.NotSerializableException;
 
+import static net.corda.serialization.internal.amqp.testutils.AMQPTestUtilsKt.testDefaultFactory;
+
 public class JavaSerialiseEnumTests {
 
     public enum Bras {
@@ -40,10 +40,7 @@ public class JavaSerialiseEnumTests {
     public void testJavaConstructorAnnotations() throws NotSerializableException {
         Bra bra = new Bra(Bras.UNDERWIRE);
 
-        SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
-                new EvolutionSerializerGetter(),
-                new SerializerFingerPrinter());
-        SerializationOutput ser = new SerializationOutput(factory1);
-        SerializedBytes<Object> bytes = ser.serialize(bra, TestSerializationContext.testSerializationContext);
+        SerializationOutput ser = new SerializationOutput(testDefaultFactory());
+        ser.serialize(bra, TestSerializationContext.testSerializationContext);
     }
 }
diff --git a/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaSerializationOutputTests.java b/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaSerializationOutputTests.java
index 365d4a5a7f..86c94a95d7 100644
--- a/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaSerializationOutputTests.java
+++ b/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaSerializationOutputTests.java
@@ -11,7 +11,6 @@
 package net.corda.serialization.internal.amqp;
 
 import net.corda.core.serialization.ConstructorForDeserialization;
-import net.corda.serialization.internal.AllWhitelist;
 import net.corda.core.serialization.SerializedBytes;
 import net.corda.serialization.internal.amqp.testutils.TestSerializationContext;
 import org.apache.qpid.proton.codec.DecoderImpl;
@@ -23,6 +22,7 @@ import java.io.NotSerializableException;
 import java.nio.ByteBuffer;
 import java.util.Objects;
 
+import static net.corda.serialization.internal.amqp.testutils.AMQPTestUtilsKt.testDefaultFactory;
 import static org.junit.Assert.assertTrue;
 
 public class JavaSerializationOutputTests {
@@ -186,14 +186,8 @@ public class JavaSerializationOutputTests {
     }
 
     private Object serdes(Object obj) throws NotSerializableException {
-        EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter();
-        FingerPrinter fingerPrinter = new SerializerFingerPrinter();
-        SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
-                evolutionSerialiserGetter,
-                fingerPrinter);
-        SerializerFactory factory2 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
-                evolutionSerialiserGetter,
-                fingerPrinter);
+        SerializerFactory factory1 = testDefaultFactory();
+        SerializerFactory factory2 = testDefaultFactory();
         SerializationOutput ser = new SerializationOutput(factory1);
         SerializedBytes<Object> bytes = ser.serialize(obj, TestSerializationContext.testSerializationContext);
 
diff --git a/serialization/src/test/java/net/corda/serialization/internal/amqp/ListsSerializationJavaTest.java b/serialization/src/test/java/net/corda/serialization/internal/amqp/ListsSerializationJavaTest.java
index e0ed6b1c67..efd9de8ea7 100644
--- a/serialization/src/test/java/net/corda/serialization/internal/amqp/ListsSerializationJavaTest.java
+++ b/serialization/src/test/java/net/corda/serialization/internal/amqp/ListsSerializationJavaTest.java
@@ -12,7 +12,6 @@ package net.corda.serialization.internal.amqp;
 
 import net.corda.core.serialization.CordaSerializable;
 import net.corda.core.serialization.SerializedBytes;
-import net.corda.serialization.internal.AllWhitelist;
 import net.corda.serialization.internal.amqp.testutils.TestSerializationContext;
 import org.junit.Assert;
 import org.junit.Test;
@@ -20,6 +19,8 @@ import org.junit.Test;
 import java.util.ArrayList;
 import java.util.List;
 
+import static net.corda.serialization.internal.amqp.testutils.AMQPTestUtilsKt.testDefaultFactory;
+
 public class ListsSerializationJavaTest {
 
     @CordaSerializable
@@ -136,12 +137,7 @@ public class ListsSerializationJavaTest {
 
     // Have to have own version as Kotlin inline functions cannot be easily called from Java
     private static <T> void assertEqualAfterRoundTripSerialization(T container, Class<T> clazz) throws Exception {
-        EvolutionSerializerGetterBase evolutionSerializerGetter = new EvolutionSerializerGetter();
-        FingerPrinter fingerPrinter = new SerializerFingerPrinter();
-        SerializerFactory factory1 = new SerializerFactory(
-                AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
-                evolutionSerializerGetter,
-                fingerPrinter);
+        SerializerFactory factory1 = testDefaultFactory();
         SerializationOutput ser = new SerializationOutput(factory1);
         SerializedBytes<Object> bytes = ser.serialize(container, TestSerializationContext.testSerializationContext);
         DeserializationInput des = new DeserializationInput(factory1);
diff --git a/serialization/src/test/java/net/corda/serialization/internal/amqp/SetterConstructorTests.java b/serialization/src/test/java/net/corda/serialization/internal/amqp/SetterConstructorTests.java
index b4ace1f3dc..747e1e19ce 100644
--- a/serialization/src/test/java/net/corda/serialization/internal/amqp/SetterConstructorTests.java
+++ b/serialization/src/test/java/net/corda/serialization/internal/amqp/SetterConstructorTests.java
@@ -11,16 +11,17 @@
 package net.corda.serialization.internal.amqp;
 
 import net.corda.core.serialization.SerializedBytes;
-import net.corda.serialization.internal.AllWhitelist;
 import net.corda.serialization.internal.amqp.testutils.TestSerializationContext;
-import org.assertj.core.api.Assertions;
 import org.junit.Test;
-import static org.junit.Assert.*;
 
 import java.io.NotSerializableException;
 import java.util.ArrayList;
 import java.util.List;
 
+import static net.corda.serialization.internal.amqp.testutils.AMQPTestUtilsKt.testDefaultFactory;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.Assert.assertEquals;
+
 public class SetterConstructorTests {
 
     static class C {
@@ -129,13 +130,7 @@ public class SetterConstructorTests {
     // despite having no constructor we should still be able to serialise an instance of C
     @Test
     public void serialiseC() throws NotSerializableException {
-        EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter();
-        FingerPrinter fingerPrinter = new SerializerFingerPrinter();
-        SerializerFactory factory1 = new SerializerFactory(
-                AllWhitelist.INSTANCE,
-                ClassLoader.getSystemClassLoader(),
-                evolutionSerialiserGetter,
-                fingerPrinter);
+        SerializerFactory factory1 = testDefaultFactory();
 
         SerializationOutput ser = new SerializationOutput(factory1);
 
@@ -205,13 +200,7 @@ public class SetterConstructorTests {
 
     @Test
     public void deserialiseC() throws NotSerializableException {
-        EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter();
-        FingerPrinter fingerPrinter = new SerializerFingerPrinter();
-        SerializerFactory factory1 = new SerializerFactory(
-                AllWhitelist.INSTANCE,
-                ClassLoader.getSystemClassLoader(),
-                evolutionSerialiserGetter,
-                fingerPrinter);
+        SerializerFactory factory1 = testDefaultFactory();
 
         C cPre1 = new C();
 
@@ -278,13 +267,7 @@ public class SetterConstructorTests {
 
     @Test
     public void serialiseOuterAndInner() throws NotSerializableException {
-        EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter();
-        FingerPrinter fingerPrinter = new SerializerFingerPrinter();
-        SerializerFactory factory1 = new SerializerFactory(
-                AllWhitelist.INSTANCE,
-                ClassLoader.getSystemClassLoader(),
-                evolutionSerialiserGetter,
-                fingerPrinter);
+        SerializerFactory factory1 = testDefaultFactory();
 
         Inner1 i1 = new Inner1("Hello");
         Inner2 i2 = new Inner2();
@@ -308,38 +291,26 @@ public class SetterConstructorTests {
 
     @Test
     public void typeMistmatch() {
-        EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter();
-        FingerPrinter fingerPrinter = new SerializerFingerPrinter();
-        SerializerFactory factory1 = new SerializerFactory(
-                AllWhitelist.INSTANCE,
-                ClassLoader.getSystemClassLoader(),
-                evolutionSerialiserGetter,
-                fingerPrinter);
+        SerializerFactory factory1 = testDefaultFactory();
 
         TypeMismatch tm = new TypeMismatch();
         tm.setA(10);
         assertEquals("10", tm.getA());
 
-        Assertions.assertThatThrownBy(() -> new SerializationOutput(factory1).serialize(tm,
+        assertThatThrownBy(() -> new SerializationOutput(factory1).serialize(tm,
                 TestSerializationContext.testSerializationContext)).isInstanceOf (
                 NotSerializableException.class);
     }
 
     @Test
     public void typeMistmatch2() {
-        EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter();
-        FingerPrinter fingerPrinter = new SerializerFingerPrinter();
-        SerializerFactory factory1 = new SerializerFactory(
-                AllWhitelist.INSTANCE,
-                ClassLoader.getSystemClassLoader(),
-                evolutionSerialiserGetter,
-                fingerPrinter);
+        SerializerFactory factory1 = testDefaultFactory();
 
         TypeMismatch2 tm = new TypeMismatch2();
         tm.setA("10");
         assertEquals((Integer)10, tm.getA());
 
-        Assertions.assertThatThrownBy(() -> new SerializationOutput(factory1).serialize(tm,
+        assertThatThrownBy(() -> new SerializationOutput(factory1).serialize(tm,
                 TestSerializationContext.testSerializationContext)).isInstanceOf(
                 NotSerializableException.class);
     }
@@ -356,13 +327,7 @@ public class SetterConstructorTests {
 
         cil.setL(l);
 
-        EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter();
-        FingerPrinter fingerPrinter = new SerializerFingerPrinter();
-        SerializerFactory factory1 = new SerializerFactory(
-                AllWhitelist.INSTANCE,
-                ClassLoader.getSystemClassLoader(),
-                evolutionSerialiserGetter,
-                fingerPrinter);
+        SerializerFactory factory1 = testDefaultFactory();
 
         // if we've got super / sub types on the setter vs the underlying type the wrong way around this will
         // explode. See CORDA-1229 (https://r3-cev.atlassian.net/browse/CORDA-1229)
diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/DeserializeNeedingCarpentrySimpleTypesTest.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/DeserializeNeedingCarpentrySimpleTypesTest.kt
index 82db042f56..8f49b15572 100644
--- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/DeserializeNeedingCarpentrySimpleTypesTest.kt
+++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/DeserializeNeedingCarpentrySimpleTypesTest.kt
@@ -11,11 +11,8 @@
 package net.corda.serialization.internal.amqp
 
 import net.corda.serialization.internal.AllWhitelist
+import net.corda.serialization.internal.amqp.testutils.*
 import net.corda.serialization.internal.carpenter.*
-import net.corda.serialization.internal.amqp.testutils.TestSerializationOutput
-import net.corda.serialization.internal.amqp.testutils.deserialize
-import net.corda.serialization.internal.amqp.testutils.serialize
-import net.corda.serialization.internal.amqp.testutils.testDefaultFactoryNoEvolution
 import org.junit.Test
 import kotlin.test.*
 
diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/DeserializeNeedingCarpentryTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/DeserializeNeedingCarpentryTests.kt
index 314c88a286..323f127207 100644
--- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/DeserializeNeedingCarpentryTests.kt
+++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/DeserializeNeedingCarpentryTests.kt
@@ -11,15 +11,13 @@
 package net.corda.serialization.internal.amqp
 
 import net.corda.core.serialization.CordaSerializable
-import net.corda.serialization.internal.carpenter.*
 import net.corda.serialization.internal.AllWhitelist
-import net.corda.serialization.internal.amqp.testutils.TestSerializationOutput
-import net.corda.serialization.internal.amqp.testutils.testDefaultFactoryNoEvolution
-import net.corda.serialization.internal.amqp.testutils.testDefaultFactoryWithWhitelist
-import net.corda.serialization.internal.amqp.testutils.serialize
-import net.corda.serialization.internal.amqp.testutils.deserialize
+import net.corda.serialization.internal.amqp.testutils.*
+import net.corda.serialization.internal.carpenter.*
 import org.junit.Test
-import kotlin.test.*
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import kotlin.test.assertTrue
 
 @CordaSerializable
 interface I {
diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/FingerPrinterTesting.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/FingerPrinterTesting.kt
index c2664caeea..8fd3faad27 100644
--- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/FingerPrinterTesting.kt
+++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/FingerPrinterTesting.kt
@@ -56,8 +56,8 @@ class FingerPrinterTestingTests {
         val factory = SerializerFactory(
                 AllWhitelist,
                 ClassLoader.getSystemClassLoader(),
-                EvolutionSerializerGetterTesting(),
-                FingerPrinterTesting())
+                evolutionSerializerGetter = EvolutionSerializerGetterTesting(),
+                fingerPrinter = FingerPrinterTesting())
 
         val blob = TestSerializationOutput(VERBOSE, factory).serializeAndReturnSchema(C(1, 2L))
 
diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationOutputTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationOutputTests.kt
index 584652afe0..969cde4a8e 100644
--- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationOutputTests.kt
+++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationOutputTests.kt
@@ -17,13 +17,7 @@ import com.nhaarman.mockito_kotlin.whenever
 import net.corda.client.rpc.RPCException
 import net.corda.core.CordaException
 import net.corda.core.CordaRuntimeException
-import net.corda.core.contracts.Amount
-import net.corda.core.contracts.Contract
-import net.corda.core.contracts.ContractAttachment
-import net.corda.core.contracts.ContractState
-import net.corda.core.contracts.PrivacySalt
-import net.corda.core.contracts.StateRef
-import net.corda.core.contracts.TransactionState
+import net.corda.core.contracts.*
 import net.corda.core.crypto.Crypto
 import net.corda.core.crypto.SecureHash
 import net.corda.core.crypto.secureRandomBytes
@@ -32,48 +26,25 @@ import net.corda.core.identity.AbstractParty
 import net.corda.core.identity.CordaX500Name
 import net.corda.core.internal.AbstractAttachment
 import net.corda.core.internal.x500Name
-import net.corda.core.serialization.ConstructorForDeserialization
-import net.corda.core.serialization.CordaSerializable
-import net.corda.core.serialization.EncodingWhitelist
-import net.corda.core.serialization.MissingAttachmentsException
-import net.corda.core.serialization.SerializationContext
-import net.corda.core.serialization.SerializationFactory
+import net.corda.core.serialization.*
 import net.corda.core.transactions.LedgerTransaction
 import net.corda.core.utilities.OpaqueBytes
 import net.corda.node.serialization.amqp.AMQPServerSerializationScheme
 import net.corda.nodeapi.internal.DEV_INTERMEDIATE_CA
 import net.corda.nodeapi.internal.crypto.ContentSignerBuilder
-import net.corda.serialization.internal.AllWhitelist
-import net.corda.serialization.internal.CordaSerializationEncoding
-import net.corda.serialization.internal.EmptyWhitelist
-import net.corda.serialization.internal.GeneratedAttachment
+import net.corda.serialization.internal.*
 import net.corda.serialization.internal.amqp.SerializerFactory.Companion.isPrimitive
-import net.corda.serialization.internal.amqp.testutils.deserialize
-import net.corda.serialization.internal.amqp.testutils.serialize
-import net.corda.serialization.internal.amqp.testutils.testDefaultFactory
-import net.corda.serialization.internal.amqp.testutils.testDefaultFactoryNoEvolution
-import net.corda.serialization.internal.amqp.testutils.testSerializationContext
-import net.corda.serialization.internal.encodingNotPermittedFormat
+import net.corda.serialization.internal.amqp.testutils.*
 import net.corda.testing.contracts.DummyContract
 import net.corda.testing.core.BOB_NAME
 import net.corda.testing.core.SerializationEnvironmentRule
 import net.corda.testing.core.TestIdentity
 import net.corda.testing.internal.rigorousMock
 import org.apache.activemq.artemis.api.core.SimpleString
-import org.apache.qpid.proton.amqp.Decimal128
-import org.apache.qpid.proton.amqp.Decimal32
-import org.apache.qpid.proton.amqp.Decimal64
-import org.apache.qpid.proton.amqp.Symbol
-import org.apache.qpid.proton.amqp.UnsignedByte
-import org.apache.qpid.proton.amqp.UnsignedInteger
-import org.apache.qpid.proton.amqp.UnsignedLong
-import org.apache.qpid.proton.amqp.UnsignedShort
+import org.apache.qpid.proton.amqp.*
 import org.apache.qpid.proton.codec.DecoderImpl
 import org.apache.qpid.proton.codec.EncoderImpl
-import org.assertj.core.api.Assertions.assertThat
-import org.assertj.core.api.Assertions.assertThatExceptionOfType
-import org.assertj.core.api.Assertions.assertThatThrownBy
-import org.assertj.core.api.Assertions.catchThrowable
+import org.assertj.core.api.Assertions.*
 import org.bouncycastle.cert.X509v2CRLBuilder
 import org.bouncycastle.cert.jcajce.JcaX509CRLConverter
 import org.bouncycastle.jce.provider.BouncyCastleProvider
@@ -89,20 +60,7 @@ import java.io.NotSerializableException
 import java.math.BigDecimal
 import java.math.BigInteger
 import java.security.cert.X509CRL
-import java.time.DayOfWeek
-import java.time.Duration
-import java.time.Instant
-import java.time.LocalDate
-import java.time.LocalDateTime
-import java.time.LocalTime
-import java.time.Month
-import java.time.MonthDay
-import java.time.OffsetDateTime
-import java.time.OffsetTime
-import java.time.Period
-import java.time.Year
-import java.time.YearMonth
-import java.time.ZonedDateTime
+import java.time.*
 import java.time.temporal.ChronoUnit
 import java.util.*
 import kotlin.reflect.full.superclasses
@@ -259,9 +217,13 @@ class SerializationOutputTests(private val compression: CordaSerializationEncodi
         if (compression != null) doReturn(true).whenever(it).acceptEncoding(compression)
     }
 
-    private fun defaultFactory() = SerializerFactory(
-            AllWhitelist, ClassLoader.getSystemClassLoader(),
-            EvolutionSerializerGetterTesting())
+    private fun defaultFactory(): SerializerFactory {
+        return SerializerFactory(
+                AllWhitelist,
+                ClassLoader.getSystemClassLoader(),
+                evolutionSerializerGetter = EvolutionSerializerGetterTesting()
+        )
+    }
 
     private inline fun <reified T : Any> serdes(obj: T,
                                                 factory: SerializerFactory = defaultFactory(),
diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/testutils/AMQPTestUtils.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/testutils/AMQPTestUtils.kt
index 8d78ed61c6..025bebdfe0 100644
--- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/testutils/AMQPTestUtils.kt
+++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/testutils/AMQPTestUtils.kt
@@ -1,16 +1,25 @@
 package net.corda.serialization.internal.amqp.testutils
 
+import net.corda.core.internal.copyTo
+import net.corda.core.internal.div
+import net.corda.core.internal.packageName
 import net.corda.core.serialization.SerializationContext
 import net.corda.core.serialization.SerializedBytes
-import org.apache.qpid.proton.codec.Data
+import net.corda.core.utilities.OpaqueBytes
 import net.corda.serialization.internal.AllWhitelist
 import net.corda.serialization.internal.EmptyWhitelist
 import net.corda.serialization.internal.amqp.*
+import net.corda.testing.common.internal.ProjectStructure
+import org.apache.qpid.proton.codec.Data
+import org.junit.Test
+import java.io.File.separatorChar
 import java.io.NotSerializableException
+import java.nio.file.StandardCopyOption.REPLACE_EXISTING
 
 fun testDefaultFactory() = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
-fun testDefaultFactoryNoEvolution() = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader(),
-        EvolutionSerializerGetterTesting())
+fun testDefaultFactoryNoEvolution(): SerializerFactory {
+    return SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader(), evolutionSerializerGetter = EvolutionSerializerGetterTesting())
+}
 fun testDefaultFactoryWithWhitelist() = SerializerFactory(EmptyWhitelist, ClassLoader.getSystemClassLoader())
 
 class TestSerializationOutput(
@@ -41,8 +50,25 @@ class TestSerializationOutput(
     }
 }
 
-fun testName(): String = Thread.currentThread().stackTrace[2].methodName
+fun testName(): String {
+    val classLoader = Thread.currentThread().contextClassLoader
+    return Thread.currentThread().stackTrace.first {
+        try {
+            classLoader.loadClass(it.className).getMethod(it.methodName).isAnnotationPresent(Test::class.java)
+        } catch (e: Exception) {
+            false
+        }
+    }.methodName
+}
 
+fun Any.testResourceName(): String = "${javaClass.simpleName}.${testName()}"
+
+fun Any.writeTestResource(bytes: OpaqueBytes) {
+    val dir = ProjectStructure.projectRootDir / "serialization" / "src" / "test" / "resources" / javaClass.packageName.replace('.', separatorChar)
+    bytes.open().copyTo(dir / testResourceName(), REPLACE_EXISTING)
+}
+
+fun Any.readTestResource(): ByteArray = javaClass.getResourceAsStream(testResourceName()).readBytes()
 
 @Throws(NotSerializableException::class)
 inline fun <reified T : Any> DeserializationInput.deserializeAndReturnEnvelope(
diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenterTest.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenterTest.kt
index 2678f0df87..6796986270 100644
--- a/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenterTest.kt
+++ b/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenterTest.kt
@@ -12,6 +12,7 @@ package net.corda.serialization.internal.carpenter
 
 import net.corda.core.internal.uncheckedCast
 import net.corda.serialization.internal.AllWhitelist
+import org.assertj.core.api.Assertions.assertThatExceptionOfType
 import org.junit.Test
 import java.beans.Introspector
 import java.lang.reflect.Field
@@ -155,21 +156,40 @@ class ClassCarpenterTest {
         assertEquals(1, i.b)
     }
 
-    @Test(expected = InterfaceMismatchException::class)
-    fun `mismatched interface`() {
-        val schema1 = ClassSchema(
+    @Test
+    fun `unimplemented interface method with lenient = false`() {
+        val schemaA = ClassSchema(
                 "gen.A",
                 mapOf("a" to NonNullableField(String::class.java)))
 
-        val schema2 = ClassSchema(
+        val schemaB = ClassSchema(
                 "gen.B",
                 mapOf("c" to NonNullableField(Int::class.java)),
-                schema1,
+                schemaA,
                 interfaces = listOf(DummyInterface::class.java))
 
-        val clazz = cc.build(schema2)
-        val i = clazz.constructors[0].newInstance("xa", 1) as DummyInterface
-        assertEquals(1, i.b)
+        assertThatExceptionOfType(InterfaceMismatchException::class.java).isThrownBy { cc.build(schemaB) }
+    }
+
+    @Test
+    fun `unimplemented interface method with lenient = true`() {
+        val cc = ClassCarpenterImpl(whitelist = AllWhitelist, lenient = true)
+
+        val schemaA = ClassSchema(
+                "gen.A",
+                mapOf("a" to NonNullableField(String::class.java)))
+
+        val schemaB = ClassSchema(
+                "gen.B",
+                mapOf("c" to NonNullableField(Int::class.java)),
+                schemaA,
+                interfaces = listOf(DummyInterface::class.java))
+
+        val classB = cc.build(schemaB)
+        val b = classB.constructors[0].newInstance("xa", 1) as DummyInterface
+        assertEquals("xa", b.a)
+        assertEquals(1, classB.getMethod("getC").invoke(b))
+        assertThatExceptionOfType(AbstractMethodError::class.java).isThrownBy { b.b }
     }
 
     @Test
diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenterTestUtils.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenterTestUtils.kt
index acd5c98aeb..07e7f9d7e4 100644
--- a/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenterTestUtils.kt
+++ b/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenterTestUtils.kt
@@ -15,6 +15,7 @@ import net.corda.serialization.internal.amqp.*
 import net.corda.serialization.internal.amqp.Field
 import net.corda.serialization.internal.amqp.Schema
 import net.corda.serialization.internal.amqp.testutils.serialize
+import net.corda.serialization.internal.amqp.testutils.testName
 
 fun mangleName(name: String) = "${name}__carpenter"
 
@@ -57,7 +58,6 @@ open class AmqpCarpenterBase(whitelist: ClassWhitelist) {
     var factory = SerializerFactoryExternalCarpenter(cc)
 
     fun serialise(clazz: Any) = SerializationOutput(factory).serialize(clazz)
-    fun testName(): String = Thread.currentThread().stackTrace[2].methodName
     @Suppress("NOTHING_TO_INLINE")
     inline fun classTestName(clazz: String) = "${this.javaClass.name}\$${testName()}\$$clazz"
 }
diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/SerDeserCarpentryTest.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/SerDeserCarpentryTest.kt
new file mode 100644
index 0000000000..fe4c06dfb9
--- /dev/null
+++ b/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/SerDeserCarpentryTest.kt
@@ -0,0 +1,49 @@
+package net.corda.serialization.internal.carpenter
+
+import net.corda.core.serialization.CordaSerializable
+import net.corda.core.serialization.SerializationFactory
+import net.corda.core.serialization.deserialize
+import net.corda.serialization.internal.amqp.testutils.readTestResource
+import net.corda.testing.core.SerializationEnvironmentRule
+import org.assertj.core.api.Assertions.assertThat
+import org.assertj.core.api.Assertions.assertThatExceptionOfType
+import org.junit.Rule
+import org.junit.Test
+
+class SerDeserCarpentryTest {
+    @Rule
+    @JvmField
+    val testSerialization = SerializationEnvironmentRule()
+
+    @Test
+    fun implementingGenericInterface() {
+        // Original class that was serialised
+//        data class GenericData(val a: Int) : GenericInterface<String>
+//        writeTestResource(GenericData(123).serialize())
+
+        val data = readTestResource().deserialize<GenericInterface<*>>()
+        assertThat(data.javaClass.getMethod("getA").invoke(data)).isEqualTo(123)
+    }
+
+    @Test
+    fun lenientCarpenter() {
+        // Original class that was serialised
+//        data class Data(val b: Int) : AInterface {
+//            override val a: Int get() = b
+//        }
+//        writeTestResource(Data(123).serialize())
+
+        val data = readTestResource().deserialize<AInterface>(context = SerializationFactory.defaultFactory.defaultContext.withLenientCarpenter())
+        assertThat(data.javaClass.getMethod("getB").invoke(data)).isEqualTo(123)
+        assertThatExceptionOfType(AbstractMethodError::class.java).isThrownBy { data.a }
+    }
+
+    @Suppress("unused")
+    @CordaSerializable
+    interface GenericInterface<T>
+
+    @CordaSerializable
+    interface AInterface {
+        val a: Int
+    }
+}
diff --git a/serialization/src/test/resources/net/corda/serialization/internal/carpenter/SerDeserCarpentryTest.implementingGenericInterface b/serialization/src/test/resources/net/corda/serialization/internal/carpenter/SerDeserCarpentryTest.implementingGenericInterface
new file mode 100644
index 0000000000..6b20aa9377
Binary files /dev/null and b/serialization/src/test/resources/net/corda/serialization/internal/carpenter/SerDeserCarpentryTest.implementingGenericInterface differ
diff --git a/serialization/src/test/resources/net/corda/serialization/internal/carpenter/SerDeserCarpentryTest.lenientCarpenter b/serialization/src/test/resources/net/corda/serialization/internal/carpenter/SerDeserCarpentryTest.lenientCarpenter
new file mode 100644
index 0000000000..112ce85fd8
Binary files /dev/null and b/serialization/src/test/resources/net/corda/serialization/internal/carpenter/SerDeserCarpentryTest.lenientCarpenter differ
diff --git a/tools/blobinspector/build.gradle b/tools/blobinspector/build.gradle
index c20270831d..26a15d84dc 100644
--- a/tools/blobinspector/build.gradle
+++ b/tools/blobinspector/build.gradle
@@ -5,7 +5,7 @@ apply plugin: 'com.jfrog.artifactory'
 
 dependencies {
     compile project(':client:jackson')
-    compile 'info.picocli:picocli:3.0.0'
+    compile "info.picocli:picocli:$picocli_version"
     compile "org.slf4j:jul-to-slf4j:$slf4j_version"
     compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"
     compile "com.jcabi:jcabi-manifests:$jcabi_manifests_version"
diff --git a/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/Main.kt b/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/Main.kt
index c65b02c6f8..9590f39d8e 100644
--- a/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/Main.kt
+++ b/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/Main.kt
@@ -7,6 +7,7 @@ import net.corda.client.jackson.JacksonSupport
 import net.corda.core.internal.isRegularFile
 import net.corda.core.internal.rootMessage
 import net.corda.core.serialization.SerializationContext
+import net.corda.core.serialization.SerializationFactory
 import net.corda.core.serialization.deserialize
 import net.corda.core.serialization.internal.SerializationEnvironmentImpl
 import net.corda.core.serialization.internal._contextSerializationEnv
@@ -33,7 +34,7 @@ fun main(args: Array<String>) {
         if (main.verbose) {
             throwable.printStackTrace()
         } else {
-            System.err.println("*ERROR*: ${throwable.rootMessage ?: "Use --verbose for more details"}")
+            System.err.println("*ERROR*: ${throwable.rootMessage}. Use --verbose for more details")
         }
         exitProcess(1)
     }
@@ -41,7 +42,7 @@ fun main(args: Array<String>) {
 
 @Command(
         name = "Blob Inspector",
-        versionProvider = VersionProvider::class,
+        versionProvider = CordaVersionProvider::class,
         mixinStandardHelpOptions = true, // add --help and --version options,
         showDefaultValues = true,
         description = ["Inspect AMQP serialised binary blobs"]
@@ -64,7 +65,9 @@ class Main : Runnable {
     var verbose: Boolean = false
 
     override fun run() {
-        System.setProperty("logLevel", if (verbose) "trace" else "off")
+        if (verbose) {
+            System.setProperty("logLevel", "trace")
+        }
 
         val bytes = source!!.readBytes().run {
             require(size > amqpMagic.size) { "Insufficient bytes for AMQP blob" }
@@ -89,7 +92,8 @@ class Main : Runnable {
         }
         val mapper = JacksonSupport.createNonRpcMapper(factory, fullParties)
 
-        val deserialized = bytes.deserialize<Any>()
+        // Deserialise with the lenient carpenter as we only care for the AMQP field getters
+        val deserialized = bytes.deserialize<Any>(context = SerializationFactory.defaultFactory.defaultContext.withLenientCarpenter())
         println(deserialized.javaClass.name)
         mapper.writeValue(System.out, deserialized)
     }
@@ -124,8 +128,13 @@ private class SourceConverter : ITypeConverter<URL> {
     }
 }
 
-private class VersionProvider : IVersionProvider {
-    override fun getVersion(): Array<String> = arrayOf(Manifests.read("Corda-Release-Version"))
+private class CordaVersionProvider : IVersionProvider {
+    override fun getVersion(): Array<String> {
+        return arrayOf(
+                "Version: ${Manifests.read("Corda-Release-Version")}",
+                "Revision: ${Manifests.read("Corda-Revision")}"
+        )
+    }
 }
 
 private enum class FormatType { YAML, JSON }
diff --git a/tools/blobinspector/src/main/resources/log4j2.xml b/tools/blobinspector/src/main/resources/log4j2.xml
index a9885efca9..98b3648e6b 100644
--- a/tools/blobinspector/src/main/resources/log4j2.xml
+++ b/tools/blobinspector/src/main/resources/log4j2.xml
@@ -1,5 +1,8 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Configuration status="info">
+    <Properties>
+        <Property name="logLevel">off</Property>
+    </Properties>
     <Appenders>
         <Console name="STDOUT" target="SYSTEM_OUT" ignoreExceptions="false">
             <PatternLayout pattern="[%C{1}.%M] %m%n"/>
diff --git a/tools/bootstrapper/build.gradle b/tools/bootstrapper/build.gradle
index 3eeb490cf3..e8bec2a7c7 100644
--- a/tools/bootstrapper/build.gradle
+++ b/tools/bootstrapper/build.gradle
@@ -8,54 +8,39 @@
  * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
  */
 
-apply plugin: 'us.kirchmeier.capsule'
+apply plugin: 'java'
+apply plugin: 'kotlin'
 apply plugin: 'net.corda.plugins.publish-utils'
 apply plugin: 'com.jfrog.artifactory'
 
 description 'Network bootstrapper'
 
-configurations {
-    runtimeArtifacts
+dependencies {
+    compile project(':node-api')
+    compile "info.picocli:picocli:$picocli_version"
+    compile "org.slf4j:jul-to-slf4j:$slf4j_version"
+    compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"
+    compile "com.jcabi:jcabi-manifests:$jcabi_manifests_version"
 }
 
 jar {
-    baseName "corda-tools-network-bootstrapper"
-}
-
-dependencies {
-    compile "org.slf4j:slf4j-nop:$slf4j_version"
-}
-
-task buildBootstrapperJar(type: FatCapsule, dependsOn: project(':node-api').compileJava) {
-    applicationClass 'net.corda.nodeapi.internal.network.NetworkBootstrapper'
-    archiveName "tools-network-bootstrapper-${corda_release_version}.jar"
-    capsuleManifest {
-        applicationVersion = corda_release_version
-        systemProperties['visualvm.display.name'] = 'Network Bootstrapper'
-        minJavaVersion = '1.8.0'
-        jvmArgs = ['-XX:+UseG1GC']
+    from(configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }) {
+        exclude "META-INF/*.SF"
+        exclude "META-INF/*.DSA"
+        exclude "META-INF/*.RSA"
     }
     from(project(':node:capsule').tasks['buildCordaJAR']) {
         rename 'corda-(.*)', 'corda.jar'
     }
-    applicationSource = files(
-            project(':node-api').configurations.runtime,
-            project(':node-api').jar
-    )
-}
-
-artifacts {
-    runtimeArtifacts buildBootstrapperJar
-    publish buildBootstrapperJar {
-        classifier ""
+    archiveName = "network-bootstrapper-${corda_release_version}.jar"
+    manifest {
+        attributes(
+                'Automatic-Module-Name': 'net.corda.bootstrapper',
+                'Main-Class': 'net.corda.bootstrapper.MainKt'
+        )
     }
 }
 
-jar {
-    classifier "ignore"
-}
-
 publish {
-    disableDefaultJar = true
     name 'corda-tools-network-bootstrapper'
 }
diff --git a/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt b/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt
new file mode 100644
index 0000000000..f80699ee65
--- /dev/null
+++ b/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt
@@ -0,0 +1,65 @@
+package net.corda.bootstrapper
+
+import com.jcabi.manifests.Manifests
+import net.corda.core.internal.rootMessage
+import net.corda.nodeapi.internal.network.NetworkBootstrapper
+import picocli.CommandLine
+import picocli.CommandLine.*
+import java.nio.file.Path
+import java.nio.file.Paths
+import kotlin.system.exitProcess
+
+fun main(args: Array<String>) {
+    val main = Main()
+    try {
+        CommandLine.run(main, *args)
+    } catch (e: ExecutionException) {
+        val throwable = e.cause ?: e
+        if (main.verbose) {
+            throwable.printStackTrace()
+        } else {
+            System.err.println("*ERROR*: ${throwable.rootMessage ?: "Use --verbose for more details"}")
+        }
+        exitProcess(1)
+    }
+}
+
+@Command(
+        name = "Network Bootstrapper",
+        versionProvider = CordaVersionProvider::class,
+        mixinStandardHelpOptions = true,
+        showDefaultValues = true,
+        description = [ "Bootstrap a local test Corda network using a set of node conf files and CorDapp JARs" ]
+)
+class Main : Runnable {
+    @Option(
+            names = ["--dir"],
+            description = [
+                "Root directory containing the node conf files and CorDapp JARs that will form the test network.",
+                "It may also contain existing node directories."
+            ]
+    )
+    private 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
+
+    @Option(names = ["--verbose"], description = ["Enable verbose output."])
+    var verbose: Boolean = false
+
+    override fun run() {
+        if (verbose) {
+            System.setProperty("logLevel", "trace")
+        }
+        NetworkBootstrapper().bootstrap(dir.toAbsolutePath().normalize(), copyCordapps = !noCopy)
+    }
+}
+
+private class CordaVersionProvider : IVersionProvider {
+    override fun getVersion(): Array<String> {
+        return arrayOf(
+                "Version: ${Manifests.read("Corda-Release-Version")}",
+                "Revision: ${Manifests.read("Corda-Revision")}"
+        )
+    }
+}
diff --git a/tools/bootstrapper/src/main/resources/log4j2.xml b/tools/bootstrapper/src/main/resources/log4j2.xml
new file mode 100644
index 0000000000..98b3648e6b
--- /dev/null
+++ b/tools/bootstrapper/src/main/resources/log4j2.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration status="info">
+    <Properties>
+        <Property name="logLevel">off</Property>
+    </Properties>
+    <Appenders>
+        <Console name="STDOUT" target="SYSTEM_OUT" ignoreExceptions="false">
+            <PatternLayout pattern="[%C{1}.%M] %m%n"/>
+        </Console>
+    </Appenders>
+    <Loggers>
+        <Root level="${sys:logLevel}">
+            <AppenderRef ref="STDOUT"/>
+        </Root>
+    </Loggers>
+</Configuration>
\ No newline at end of file
diff --git a/webserver/src/main/kotlin/net/corda/webserver/WebServer.kt b/webserver/src/main/kotlin/net/corda/webserver/WebServer.kt
index 5eb0fa1f97..6657e57631 100644
--- a/webserver/src/main/kotlin/net/corda/webserver/WebServer.kt
+++ b/webserver/src/main/kotlin/net/corda/webserver/WebServer.kt
@@ -14,6 +14,7 @@ package net.corda.webserver
 
 import com.typesafe.config.ConfigException
 import net.corda.core.internal.div
+import net.corda.core.internal.location
 import net.corda.core.internal.rootCause
 import net.corda.webserver.internal.NodeWebServer
 import org.slf4j.LoggerFactory
@@ -58,7 +59,7 @@ fun main(args: Array<String>) {
         exitProcess(2)
     }
 
-    log.info("Main class: ${WebServerConfig::class.java.protectionDomain.codeSource.location.toURI().path}")
+    log.info("Main class: ${WebServerConfig::class.java.location.toURI().path}")
     val info = ManagementFactory.getRuntimeMXBean()
     log.info("CommandLine Args: ${info.inputArguments.joinToString(" ")}")
     log.info("Application Args: ${args.joinToString(" ")}")