mirror of
https://github.com/corda/corda.git
synced 2025-04-12 05:40:48 +00:00
Merge pull request #1773 from corda/clint/v1.0-changes
Various fixes cherry picked from V1
This commit is contained in:
commit
035bd48eec
18
README.md
18
README.md
@ -8,25 +8,25 @@ Corda is a decentralised database system in which nodes trust each other as litt
|
||||
|
||||
## Features
|
||||
|
||||
* A P2P network of nodes
|
||||
* Smart contracts
|
||||
* Flow framework
|
||||
* "Notary" infrastructure to validate uniqueness of transactions
|
||||
* Written as a platform for distributed apps called CorDapps
|
||||
* Smart contracts that can be written in Java and other JVM languages
|
||||
* Flow framework to manage communication and negotiation between participants
|
||||
* Peer-to-peer network of nodes
|
||||
* "Notary" infrastructure to validate uniqueness and sequencing of transactions without global broadcast
|
||||
* Enables the development and deployment of distributed apps called CorDapps
|
||||
* Written in [Kotlin](https://kotlinlang.org), targeting the JVM
|
||||
|
||||
## Getting started
|
||||
|
||||
1. Read the [Getting started](https://docs.corda.net/getting-set-up.html) documentation
|
||||
2. Run the [example CorDapp](https://docs.corda.net/tutorial-cordapp.html)
|
||||
1. Read the [Getting Started](https://docs.corda.net/getting-set-up.html) documentation
|
||||
2. Run the [Example CorDapp](https://docs.corda.net/tutorial-cordapp.html)
|
||||
3. Read about Corda's [Key Concepts](https://docs.corda.net/key-concepts.html)
|
||||
3. Follow the [Hello, World! tutorial](https://docs.corda.net/hello-world-index.html)
|
||||
|
||||
## Useful links
|
||||
|
||||
* [Project website](https://corda.net)
|
||||
* [Project Website](https://corda.net)
|
||||
* [Documentation](https://docs.corda.net)
|
||||
* [Slack channel](https://slack.corda.net/)
|
||||
* [Slack Channel](https://slack.corda.net/)
|
||||
* [Stack Overflow tag](https://stackoverflow.com/questions/tagged/corda)
|
||||
* [Forum](https://discourse.corda.net)
|
||||
* [Meetups](https://www.meetup.com/pro/corda/)
|
||||
|
@ -24,16 +24,4 @@ class IllegalFlowLogicException(type: Class<*>, msg: String) : IllegalArgumentEx
|
||||
*/
|
||||
// TODO: align this with the existing [FlowRef] in the bank-side API (probably replace some of the API classes)
|
||||
@CordaSerializable
|
||||
interface FlowLogicRef
|
||||
|
||||
|
||||
/**
|
||||
* This is just some way to track what attachments need to be in the class loader, but may later include some app
|
||||
* properties loaded from the attachments. And perhaps the authenticated user for an API call?
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class AppContext(val attachments: List<SecureHash>) {
|
||||
// TODO: build a real [AttachmentsClassLoader] etc
|
||||
val classLoader: ClassLoader
|
||||
get() = this.javaClass.classLoader
|
||||
}
|
||||
interface FlowLogicRef
|
@ -28,12 +28,6 @@ Internal, do not use. These APIs and implementations which are currently being r
|
||||
|
||||
Exception types and some utilities for working with observables and futures.
|
||||
|
||||
# Package net.corda.core.cordapp
|
||||
|
||||
This package contains the interface to CorDapps from within a node. A CorDapp can access its own context by using
|
||||
the CordappProvider.getAppContext() class. These classes are not intended to be constructed manually and no interface
|
||||
to do this will be provided.
|
||||
|
||||
# Package net.corda.core.concurrent
|
||||
|
||||
Provides a simplified [java.util.concurrent.Future] class that allows registration of a callback to execute when the future
|
||||
@ -47,6 +41,12 @@ with [Contract], or see the examples in [net.corda.finance.contracts].
|
||||
Corda smart contracts are a combination of state held on the distributed ledger, and verification logic which defines
|
||||
which transformations of state are valid.
|
||||
|
||||
# Package net.corda.core.cordapp
|
||||
|
||||
This package contains the interface to CorDapps from within a node. A CorDapp can access it's own context by using
|
||||
the CordappProvider.getAppContext() class. These classes are not intended to be constructed manually and no interface
|
||||
to do this will be provided.
|
||||
|
||||
# Package net.corda.core.crypto
|
||||
|
||||
Cryptography data and utility classes used for signing, verifying, key management and data integrity checks.
|
||||
|
@ -15,7 +15,7 @@ A typical constraint is the hash of the CorDapp JAR that contains the contract a
|
||||
include constraints that require specific signers of the JAR, or both the signer and the hash. Constraints can be
|
||||
specified when constructing a transaction; if unspecified, an automatic constraint is used.
|
||||
|
||||
A ``TransactionState`` has a ``constraint`` field that represents that state's attachment constraint. When a party
|
||||
``TransactionState``s have a ``constraint`` field that represents that state's attachment constraint. When a party
|
||||
constructs a ``TransactionState`` without specifying the constraint parameter a default value
|
||||
(``AutomaticHashConstraint``) is used. This default will be automatically resolved to a specific
|
||||
``HashAttachmentConstraint`` that contains the hash of the attachment which contains the contract of that
|
||||
@ -80,8 +80,8 @@ attachment JAR. This allows for trusting of attachments from trusted entities.
|
||||
Limitations
|
||||
-----------
|
||||
|
||||
An ``AttachmentConstraint`` is verified by running the ``AttachmentConstraint.isSatisfiedBy`` method. When this is
|
||||
called it is provided only the relevant attachment by the transaction that is verifying it.
|
||||
``AttachmentConstraint``s are verified by running the ``AttachmentConstraint.isSatisfiedBy`` method. When this is called
|
||||
it is provided only the relevant attachment by the transaction that is verifying it.
|
||||
|
||||
Testing
|
||||
-------
|
||||
|
@ -9,6 +9,7 @@ import net.corda.core.identity.Party
|
||||
* loaded or blocked.
|
||||
*/
|
||||
class IsolatedDummyFlow {
|
||||
@StartableByRPC
|
||||
@InitiatingFlow
|
||||
class Initiator(val toWhom: Party) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
@ -31,4 +32,4 @@ class IsolatedDummyFlow {
|
||||
stx.verify(serviceHub)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -521,7 +521,7 @@ object LoggerSerializer : Serializer<Logger>() {
|
||||
object ClassSerializer : Serializer<Class<*>>() {
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<Class<*>>): Class<*> {
|
||||
val className = input.readString()
|
||||
return Class.forName(className)
|
||||
return Class.forName(className, true, kryo.classLoader)
|
||||
}
|
||||
|
||||
override fun write(kryo: Kryo, output: Output, clazz: Class<*>) {
|
||||
|
@ -1,10 +1,12 @@
|
||||
package net.corda.node.services
|
||||
|
||||
import net.corda.client.rpc.RPCException
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.contracts.Contract
|
||||
import net.corda.core.contracts.PartyAndReference
|
||||
import net.corda.core.cordapp.CordappProvider
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.UnexpectedFlowEndException
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.concurrent.transpose
|
||||
@ -16,12 +18,18 @@ import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.node.internal.cordapp.CordappLoader
|
||||
import net.corda.node.internal.cordapp.CordappProviderImpl
|
||||
import net.corda.node.services.transactions.SimpleNotaryService
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.nodeapi.internal.ServiceInfo
|
||||
import net.corda.testing.DUMMY_BANK_A
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.testing.TestDependencyInjectionBase
|
||||
import net.corda.testing.driver.DriverDSL
|
||||
import net.corda.testing.driver.DriverDSLExposedInterface
|
||||
import net.corda.testing.driver.NodeHandle
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.resetTestSerialization
|
||||
@ -30,6 +38,8 @@ import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.net.URLClassLoader
|
||||
import java.nio.file.Files
|
||||
import java.sql.Driver
|
||||
import kotlin.test.assertFails
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class AttachmentLoadingTests : TestDependencyInjectionBase() {
|
||||
@ -45,6 +55,13 @@ class AttachmentLoadingTests : TestDependencyInjectionBase() {
|
||||
val logger = loggerFor<AttachmentLoadingTests>()
|
||||
val isolatedJAR = AttachmentLoadingTests::class.java.getResource("isolated.jar")!!
|
||||
val ISOLATED_CONTRACT_ID = "net.corda.finance.contracts.isolated.AnotherDummyContract"
|
||||
|
||||
val bankAName = CordaX500Name("BankA", "Zurich", "CH")
|
||||
val bankBName = CordaX500Name("BankB", "Zurich", "CH")
|
||||
val notaryName = CordaX500Name("Notary", "Zurich", "CH")
|
||||
val flowInitiatorClass =
|
||||
Class.forName("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator", true, URLClassLoader(arrayOf(isolatedJAR)))
|
||||
.asSubclass(FlowLogic::class.java)
|
||||
}
|
||||
|
||||
private lateinit var services: Services
|
||||
@ -75,40 +92,44 @@ class AttachmentLoadingTests : TestDependencyInjectionBase() {
|
||||
@Test
|
||||
fun `test that attachments retrieved over the network are not used for code`() {
|
||||
driver(initialiseSerialization = false) {
|
||||
val bankAName = CordaX500Name("BankA", "Zurich", "CH")
|
||||
val bankBName = CordaX500Name("BankB", "Zurich", "CH")
|
||||
// Copy the app jar to the first node. The second won't have it.
|
||||
val path = (baseDirectory(bankAName.toString()) / "plugins").createDirectories() / "isolated.jar"
|
||||
logger.info("Installing isolated jar to $path")
|
||||
isolatedJAR.openStream().buffered().use { input ->
|
||||
Files.newOutputStream(path).buffered().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
installIsolatedCordappTo(bankAName)
|
||||
val (bankA, bankB, _) = createTwoNodesAndNotary()
|
||||
|
||||
val admin = User("admin", "admin", permissions = setOf("ALL"))
|
||||
val (bankA, bankB) = listOf(
|
||||
startNode(providedName = bankAName, rpcUsers = listOf(admin)),
|
||||
startNode(providedName = bankBName, rpcUsers = listOf(admin))
|
||||
).transpose().getOrThrow() // Wait for all nodes to start up.
|
||||
|
||||
val clazz =
|
||||
Class.forName("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator", true, URLClassLoader(arrayOf(isolatedJAR)))
|
||||
.asSubclass(FlowLogic::class.java)
|
||||
|
||||
try {
|
||||
bankA.rpcClientToNode().start("admin", "admin").use { rpc ->
|
||||
val proxy = rpc.proxy
|
||||
val party = proxy.wellKnownPartyFromX500Name(bankBName)!!
|
||||
|
||||
assertFailsWith<RPCException>("net.corda.client.rpc.RPCException: net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator") {
|
||||
proxy.startFlowDynamic(clazz, party).returnValue.getOrThrow()
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
bankA.stop()
|
||||
bankB.stop()
|
||||
assertFailsWith<UnexpectedFlowEndException>("Party C=CH,L=Zurich,O=BankB rejected session request: Don't know net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator") {
|
||||
bankA.rpc.startFlowDynamic(flowInitiatorClass, bankB.nodeInfo.legalIdentities.first()).returnValue.getOrThrow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `tests that if the attachment is loaded on both sides already that a flow can run`() {
|
||||
driver(initialiseSerialization = false) {
|
||||
installIsolatedCordappTo(bankAName)
|
||||
installIsolatedCordappTo(bankBName)
|
||||
val (bankA, bankB, _) = createTwoNodesAndNotary()
|
||||
bankA.rpc.startFlowDynamic(flowInitiatorClass, bankB.nodeInfo.legalIdentities.first()).returnValue.getOrThrow()
|
||||
}
|
||||
}
|
||||
|
||||
private fun DriverDSLExposedInterface.installIsolatedCordappTo(nodeName: CordaX500Name) {
|
||||
// Copy the app jar to the first node. The second won't have it.
|
||||
val path = (baseDirectory(nodeName.toString()) / "plugins").createDirectories() / "isolated.jar"
|
||||
logger.info("Installing isolated jar to $path")
|
||||
isolatedJAR.openStream().buffered().use { input ->
|
||||
Files.newOutputStream(path).buffered().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DriverDSLExposedInterface.createTwoNodesAndNotary(): List<NodeHandle> {
|
||||
val adminUser = User("admin", "admin", permissions = setOf("ALL"))
|
||||
val nodes = listOf(
|
||||
startNode(providedName = bankAName, rpcUsers = listOf(adminUser)),
|
||||
startNode(providedName = bankBName, rpcUsers = listOf(adminUser)),
|
||||
startNode(providedName = notaryName, rpcUsers = listOf(adminUser), advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
|
||||
).transpose().getOrThrow() // Wait for all nodes to start up.
|
||||
nodes.forEach { it.rpc.waitUntilNetworkReady() }
|
||||
return nodes
|
||||
}
|
||||
}
|
||||
|
Binary file not shown.
@ -58,10 +58,7 @@ import net.corda.node.services.persistence.DBTransactionStorage
|
||||
import net.corda.node.services.persistence.NodeAttachmentService
|
||||
import net.corda.node.services.schema.HibernateObserver
|
||||
import net.corda.node.services.schema.NodeSchemaService
|
||||
import net.corda.node.services.statemachine.FlowStateMachineImpl
|
||||
import net.corda.node.services.statemachine.StateMachineManager
|
||||
import net.corda.node.services.statemachine.appName
|
||||
import net.corda.node.services.statemachine.flowVersionAndInitiatingClass
|
||||
import net.corda.node.services.statemachine.*
|
||||
import net.corda.node.services.transactions.*
|
||||
import net.corda.node.services.upgrade.ContractUpgradeServiceImpl
|
||||
import net.corda.node.services.vault.NodeVaultService
|
||||
@ -190,7 +187,8 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
||||
checkpointStorage,
|
||||
serverThread,
|
||||
database,
|
||||
busyNodeLatch)
|
||||
busyNodeLatch,
|
||||
cordappLoader.appClassLoader)
|
||||
|
||||
smm.tokenizableServices.addAll(tokenizableServices)
|
||||
|
||||
@ -213,6 +211,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
||||
registerCordappFlows()
|
||||
_services.rpcFlows += cordappProvider.cordapps.flatMap { it.rpcFlows }
|
||||
registerCustomSchemas(cordappProvider.cordapps.flatMap { it.customSchemas }.toSet())
|
||||
FlowLogicRefFactoryImpl.classloader = cordappLoader.appClassLoader
|
||||
|
||||
runOnStop += network::stop
|
||||
StartedNodeImpl(this, _services, info, checkpointStorage, smm, attachments, inNodeNetworkMapService, network, database, rpcOps)
|
||||
|
@ -2,6 +2,7 @@ package net.corda.node.services.statemachine
|
||||
|
||||
import net.corda.core.internal.VisibleForTesting
|
||||
import com.google.common.primitives.Primitives
|
||||
import net.corda.core.cordapp.CordappContext
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
@ -17,7 +18,7 @@ import kotlin.reflect.jvm.javaType
|
||||
* The internal concrete implementation of the FlowLogicRef marker interface.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class FlowLogicRefImpl internal constructor(val flowLogicClassName: String, val appContext: AppContext, val args: Map<String, Any?>) : FlowLogicRef
|
||||
data class FlowLogicRefImpl internal constructor(val flowLogicClassName: String, val args: Map<String, Any?>) : FlowLogicRef
|
||||
|
||||
/**
|
||||
* A class for conversion to and from [FlowLogic] and [FlowLogicRef] instances.
|
||||
@ -32,6 +33,9 @@ data class FlowLogicRefImpl internal constructor(val flowLogicClassName: String,
|
||||
* in response to a potential malicious use or buggy update to an app etc.
|
||||
*/
|
||||
object FlowLogicRefFactoryImpl : SingletonSerializeAsToken(), FlowLogicRefFactory {
|
||||
// TODO: Replace with a per app classloader/cordapp provider/cordapp loader - this will do for now
|
||||
var classloader = javaClass.classLoader
|
||||
|
||||
override fun create(flowClass: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef {
|
||||
if (!flowClass.isAnnotationPresent(SchedulableFlow::class.java)) {
|
||||
throw IllegalFlowLogicException(flowClass, "because it's not a schedulable flow")
|
||||
@ -73,17 +77,14 @@ object FlowLogicRefFactoryImpl : SingletonSerializeAsToken(), FlowLogicRefFactor
|
||||
*/
|
||||
@VisibleForTesting
|
||||
internal fun createKotlin(type: Class<out FlowLogic<*>>, args: Map<String, Any?>): FlowLogicRef {
|
||||
// TODO: we need to capture something about the class loader or "application context" into the ref,
|
||||
// perhaps as some sort of ThreadLocal style object. For now, just create an empty one.
|
||||
val appContext = AppContext(emptyList())
|
||||
// Check we can find a constructor and populate the args to it, but don't call it
|
||||
createConstructor(type, args)
|
||||
return FlowLogicRefImpl(type.name, appContext, args)
|
||||
return FlowLogicRefImpl(type.name, args)
|
||||
}
|
||||
|
||||
fun toFlowLogic(ref: FlowLogicRef): FlowLogic<*> {
|
||||
if (ref !is FlowLogicRefImpl) throw IllegalFlowLogicException(ref.javaClass, "FlowLogicRef was not created via correct FlowLogicRefFactory interface")
|
||||
val klass = Class.forName(ref.flowLogicClassName, true, ref.appContext.classLoader).asSubclass(FlowLogic::class.java)
|
||||
val klass = Class.forName(ref.flowLogicClassName, true, classloader).asSubclass(FlowLogic::class.java)
|
||||
return createConstructor(klass, ref.args)()
|
||||
}
|
||||
|
||||
|
@ -77,7 +77,8 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
|
||||
val checkpointStorage: CheckpointStorage,
|
||||
val executor: AffinityExecutor,
|
||||
val database: CordaPersistence,
|
||||
private val unfinishedFibers: ReusableLatch = ReusableLatch()) {
|
||||
private val unfinishedFibers: ReusableLatch = ReusableLatch(),
|
||||
private val classloader: ClassLoader = javaClass.classLoader) {
|
||||
|
||||
inner class FiberScheduler : FiberExecutorScheduler("Same thread scheduler", executor)
|
||||
|
||||
@ -377,7 +378,12 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
|
||||
updateCheckpoint(fiber)
|
||||
session to initiatedFlowFactory
|
||||
} catch (e: SessionRejectException) {
|
||||
logger.warn("${e.logMessage}: $sessionInit")
|
||||
// TODO: Handle this more gracefully
|
||||
try {
|
||||
logger.warn("${e.logMessage}: $sessionInit")
|
||||
} catch (e: Throwable) {
|
||||
logger.warn("Problematic session init message during logging", e)
|
||||
}
|
||||
sendSessionReject(e.rejectMessage)
|
||||
return
|
||||
} catch (e: Exception) {
|
||||
@ -400,7 +406,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
|
||||
|
||||
private fun getInitiatedFlowFactory(sessionInit: SessionInit): InitiatedFlowFactory<*> {
|
||||
val initiatingFlowClass = try {
|
||||
Class.forName(sessionInit.initiatingFlowClass).asSubclass(FlowLogic::class.java)
|
||||
Class.forName(sessionInit.initiatingFlowClass, true, classloader).asSubclass(FlowLogic::class.java)
|
||||
} catch (e: ClassNotFoundException) {
|
||||
throw SessionRejectException("Don't know ${sessionInit.initiatingFlowClass}")
|
||||
} catch (e: ClassCastException) {
|
||||
|
@ -55,6 +55,8 @@ class CordappLoaderTest {
|
||||
assertThat(actualCordapp.rpcFlows).isEmpty()
|
||||
assertThat(actualCordapp.schedulableFlows).isEmpty()
|
||||
assertThat(actualCordapp.services).isEmpty()
|
||||
assertThat(actualCordapp.serializationWhitelists).hasSize(1)
|
||||
assertThat(actualCordapp.serializationWhitelists.first().javaClass.name).isEqualTo("net.corda.nodeapi.internal.serialization.DefaultWhitelist")
|
||||
assertThat(actualCordapp.jarPath).isEqualTo(isolatedJAR)
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user