,
+ protected val proxyClass: Class,
+ protected val factory: SerializerFactory,
+ val withInheritance: Boolean = true) : CustomSerializer() {
+ override fun isSerializerFor(clazz: Class<*>): Boolean = if (withInheritance) this.clazz.isAssignableFrom(clazz) else this.clazz == clazz
+ override val type: Type get() = clazz
+ override val typeDescriptor: String = "$DESCRIPTOR_DOMAIN:${clazz.name}"
+ override fun writeClassInfo(output: SerializationOutput) {}
+ override val descriptor: Descriptor = Descriptor(typeDescriptor)
+
+ private val proxySerializer: ObjectSerializer by lazy { ObjectSerializer(proxyClass, factory) }
+
+ override val schemaForDocumentation: Schema by lazy {
+ val typeNotations = mutableSetOf(CompositeType(type.typeName, null, emptyList(), descriptor, (proxySerializer.typeNotation as CompositeType).fields))
+ for (additional in additionalSerializers) {
+ typeNotations.addAll(additional.schemaForDocumentation.types)
+ }
+ Schema(typeNotations.toList())
+ }
+
+ /**
+ * Implement these two methods.
+ */
+ protected abstract fun toProxy(obj: T): P
+
+ protected abstract fun fromProxy(proxy: P): T
+
+ override fun writeDescribedObject(obj: T, data: Data, type: Type, output: SerializationOutput) {
+ val proxy = toProxy(obj)
+ data.withList {
+ for (property in proxySerializer.propertySerializers) {
+ property.writeProperty(proxy, this, output)
+ }
+ }
+ }
+
+ override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): T {
+ @Suppress("UNCHECKED_CAST")
+ val proxy = proxySerializer.readObject(obj, schema, input) as P
+ return fromProxy(proxy)
+ }
+ }
+}
diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/DeserializationInput.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/DeserializationInput.kt
index b47d75b8bc..ccbe1fac20 100644
--- a/core/src/main/kotlin/net/corda/core/serialization/amqp/DeserializationInput.kt
+++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/DeserializationInput.kt
@@ -15,7 +15,7 @@ import java.util.*
* @param serializerFactory This is the factory for [AMQPSerializer] instances and can be shared across multiple
* instances and threads.
*/
-class DeserializationInput(private val serializerFactory: SerializerFactory = SerializerFactory()) {
+class DeserializationInput(internal val serializerFactory: SerializerFactory = SerializerFactory()) {
// TODO: we're not supporting object refs yet
private val objectHistory: MutableList = ArrayList()
@@ -41,7 +41,7 @@ class DeserializationInput(private val serializerFactory: SerializerFactory = Se
}
val envelope = Envelope.get(data)
// Now pick out the obj and schema from the envelope.
- return clazz.cast(readObjectOrNull(envelope.obj, envelope, clazz))
+ return clazz.cast(readObjectOrNull(envelope.obj, envelope.schema, clazz))
} catch(nse: NotSerializableException) {
throw nse
} catch(t: Throwable) {
@@ -51,20 +51,21 @@ class DeserializationInput(private val serializerFactory: SerializerFactory = Se
}
}
- internal fun readObjectOrNull(obj: Any?, envelope: Envelope, type: Type): Any? {
+ internal fun readObjectOrNull(obj: Any?, schema: Schema, type: Type): Any? {
if (obj == null) {
return null
} else {
- return readObject(obj, envelope, type)
+ return readObject(obj, schema, type)
}
}
- internal fun readObject(obj: Any, envelope: Envelope, type: Type): Any {
+ internal fun readObject(obj: Any, schema: Schema, type: Type): Any {
if (obj is DescribedType) {
// Look up serializer in factory by descriptor
- val serializer = serializerFactory.get(obj.descriptor, envelope)
- if (serializer.type != type && !serializer.type.isSubClassOf(type)) throw NotSerializableException("Described type with descriptor ${obj.descriptor} was expected to be of type $type")
- return serializer.readObject(obj.described, envelope, this)
+ val serializer = serializerFactory.get(obj.descriptor, schema)
+ if (serializer.type != type && !serializer.type.isSubClassOf(type))
+ throw NotSerializableException("Described type with descriptor ${obj.descriptor} was expected to be of type $type")
+ return serializer.readObject(obj.described, schema, this)
} else {
return obj
}
diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/DeserializedParameterizedType.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/DeserializedParameterizedType.kt
index 2cd0ae1298..9a0809d18d 100644
--- a/core/src/main/kotlin/net/corda/core/serialization/amqp/DeserializedParameterizedType.kt
+++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/DeserializedParameterizedType.kt
@@ -119,7 +119,7 @@ class DeserializedParameterizedType(private val rawType: Class<*>, private val p
private fun makeType(typeName: String, cl: ClassLoader): Type {
// Not generic
- return if (typeName == "*") SerializerFactory.AnyType else Class.forName(typeName, false, cl)
+ return if (typeName == "?") SerializerFactory.AnyType else Class.forName(typeName, false, cl)
}
private fun makeParameterizedType(rawTypeName: String, args: MutableList, cl: ClassLoader): Type {
diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/MapSerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/MapSerializer.kt
index 2ea61c6598..7991648f1a 100644
--- a/core/src/main/kotlin/net/corda/core/serialization/amqp/MapSerializer.kt
+++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/MapSerializer.kt
@@ -13,10 +13,9 @@ import kotlin.collections.map
/**
* Serialization / deserialization of certain supported [Map] types.
*/
-class MapSerializer(val declaredType: ParameterizedType) : AMQPSerializer {
+class MapSerializer(val declaredType: ParameterizedType, factory: SerializerFactory) : AMQPSerializer {
override val type: Type = declaredType as? DeserializedParameterizedType ?: DeserializedParameterizedType.make(declaredType.toString())
- private val typeName = declaredType.toString()
- override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type)}"
+ override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}"
companion object {
private val supportedTypes: Map>, (Map<*, *>) -> Map<*, *>> = mapOf(
@@ -24,15 +23,15 @@ class MapSerializer(val declaredType: ParameterizedType) : AMQPSerializer {
SortedMap::class.java to { map -> Collections.unmodifiableSortedMap(TreeMap(map)) },
NavigableMap::class.java to { map -> Collections.unmodifiableNavigableMap(TreeMap(map)) }
)
+
+ private fun findConcreteType(clazz: Class<*>): (Map<*, *>) -> Map<*, *> {
+ return supportedTypes[clazz] ?: throw NotSerializableException("Unsupported map type $clazz.")
+ }
}
private val concreteBuilder: (Map<*, *>) -> Map<*, *> = findConcreteType(declaredType.rawType as Class<*>)
- private fun findConcreteType(clazz: Class<*>): (Map<*, *>) -> Map<*, *> {
- return supportedTypes[clazz] ?: throw NotSerializableException("Unsupported map type $clazz.")
- }
-
- private val typeNotation: TypeNotation = RestrictedType(typeName, null, emptyList(), "map", Descriptor(typeDescriptor, null), emptyList())
+ private val typeNotation: TypeNotation = RestrictedType(declaredType.toString(), null, emptyList(), "map", Descriptor(typeDescriptor, null), emptyList())
override fun writeClassInfo(output: SerializationOutput) {
if (output.writeTypeNotations(typeNotation)) {
@@ -56,11 +55,13 @@ class MapSerializer(val declaredType: ParameterizedType) : AMQPSerializer {
}
}
- override fun readObject(obj: Any, envelope: Envelope, input: DeserializationInput): Any {
+ override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any {
// TODO: General generics question. Do we need to validate that entries in Maps and Collections match the generic type? Is it a security hole?
- val entries: Iterable> = (obj as Map<*, *>).map { readEntry(envelope, input, it) }
+ val entries: Iterable> = (obj as Map<*, *>).map { readEntry(schema, input, it) }
return concreteBuilder(entries.toMap())
}
- private fun readEntry(envelope: Envelope, input: DeserializationInput, entry: Map.Entry) = input.readObjectOrNull(entry.key, envelope, declaredType.actualTypeArguments[0]) to input.readObjectOrNull(entry.value, envelope, declaredType.actualTypeArguments[1])
+ private fun readEntry(schema: Schema, input: DeserializationInput, entry: Map.Entry) =
+ input.readObjectOrNull(entry.key, schema, declaredType.actualTypeArguments[0]) to
+ input.readObjectOrNull(entry.value, schema, declaredType.actualTypeArguments[1])
}
\ No newline at end of file
diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/ObjectSerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/ObjectSerializer.kt
index 2ccfad81d6..130d50d7a3 100644
--- a/core/src/main/kotlin/net/corda/core/serialization/amqp/ObjectSerializer.kt
+++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/ObjectSerializer.kt
@@ -10,26 +10,30 @@ import kotlin.reflect.jvm.javaConstructor
/**
* Responsible for serializing and deserializing a regular object instance via a series of properties (matched with a constructor).
*/
-class ObjectSerializer(val clazz: Class<*>) : AMQPSerializer {
+class ObjectSerializer(val clazz: Class<*>, factory: SerializerFactory) : AMQPSerializer {
override val type: Type get() = clazz
private val javaConstructor: Constructor?
- private val propertySerializers: Collection
+ internal val propertySerializers: Collection
init {
val kotlinConstructor = constructorForDeserialization(clazz)
javaConstructor = kotlinConstructor?.javaConstructor
- propertySerializers = propertiesForSerialization(kotlinConstructor, clazz)
+ propertySerializers = propertiesForSerialization(kotlinConstructor, clazz, factory)
}
private val typeName = clazz.name
- override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type)}"
+ override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}"
private val interfaces = interfacesForSerialization(clazz) // TODO maybe this proves too much and we need annotations to restrict.
- private val typeNotation: TypeNotation = CompositeType(typeName, null, generateProvides(), Descriptor(typeDescriptor, null), generateFields())
+ internal val typeNotation: TypeNotation = CompositeType(typeName, null, generateProvides(), Descriptor(typeDescriptor, null), generateFields())
override fun writeClassInfo(output: SerializationOutput) {
- output.writeTypeNotations(typeNotation)
- for (iface in interfaces) {
- output.requireSerializer(iface)
+ if (output.writeTypeNotations(typeNotation)) {
+ for (iface in interfaces) {
+ output.requireSerializer(iface)
+ }
+ for (property in propertySerializers) {
+ property.writeClassInfo(output)
+ }
}
}
@@ -45,13 +49,13 @@ class ObjectSerializer(val clazz: Class<*>) : AMQPSerializer {
}
}
- override fun readObject(obj: Any, envelope: Envelope, input: DeserializationInput): Any {
+ override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any {
if (obj is UnsignedInteger) {
// TODO: Object refs
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
} else if (obj is List<*>) {
if (obj.size > propertySerializers.size) throw NotSerializableException("Too many properties in described type $typeName")
- val params = obj.zip(propertySerializers).map { it.second.readProperty(it.first, envelope, input) }
+ val params = obj.zip(propertySerializers).map { it.second.readProperty(it.first, schema, input) }
return construct(params)
} else throw NotSerializableException("Body of described type is unexpected $obj")
}
diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/PropertySerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/PropertySerializer.kt
index 50cb6c5581..2295a07b45 100644
--- a/core/src/main/kotlin/net/corda/core/serialization/amqp/PropertySerializer.kt
+++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/PropertySerializer.kt
@@ -9,8 +9,9 @@ import kotlin.reflect.jvm.javaGetter
* Base class for serialization of a property of an object.
*/
sealed class PropertySerializer(val name: String, val readMethod: Method) {
+ abstract fun writeClassInfo(output: SerializationOutput)
abstract fun writeProperty(obj: Any?, data: Data, output: SerializationOutput)
- abstract fun readProperty(obj: Any?, envelope: Envelope, input: DeserializationInput): Any?
+ abstract fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any?
val type: String = generateType()
val requires: List = generateRequires()
@@ -53,13 +54,13 @@ sealed class PropertySerializer(val name: String, val readMethod: Method) {
}
companion object {
- fun make(name: String, readMethod: Method): PropertySerializer {
+ fun make(name: String, readMethod: Method, factory: SerializerFactory): PropertySerializer {
val type = readMethod.genericReturnType
if (SerializerFactory.isPrimitive(type)) {
// This is a little inefficient for performance since it does a runtime check of type. We could do build time check with lots of subclasses here.
return AMQPPrimitivePropertySerializer(name, readMethod)
} else {
- return DescribedTypePropertySerializer(name, readMethod)
+ return DescribedTypePropertySerializer(name, readMethod) { factory.get(null, type) }
}
}
}
@@ -67,9 +68,16 @@ sealed class PropertySerializer(val name: String, val readMethod: Method) {
/**
* A property serializer for a complex type (another object).
*/
- class DescribedTypePropertySerializer(name: String, readMethod: Method) : PropertySerializer(name, readMethod) {
- override fun readProperty(obj: Any?, envelope: Envelope, input: DeserializationInput): Any? {
- return input.readObjectOrNull(obj, envelope, readMethod.genericReturnType)
+ class DescribedTypePropertySerializer(name: String, readMethod: Method, private val lazyTypeSerializer: () -> AMQPSerializer) : PropertySerializer(name, readMethod) {
+ // This is lazy so we don't get an infinite loop when a method returns an instance of the class.
+ private val typeSerializer: AMQPSerializer by lazy { lazyTypeSerializer() }
+
+ override fun writeClassInfo(output: SerializationOutput) {
+ typeSerializer.writeClassInfo(output)
+ }
+
+ override fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? {
+ return input.readObjectOrNull(obj, schema, readMethod.genericReturnType)
}
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) {
@@ -81,7 +89,9 @@ sealed class PropertySerializer(val name: String, val readMethod: Method) {
* A property serializer for an AMQP primitive type (Int, String, etc).
*/
class AMQPPrimitivePropertySerializer(name: String, readMethod: Method) : PropertySerializer(name, readMethod) {
- override fun readProperty(obj: Any?, envelope: Envelope, input: DeserializationInput): Any? {
+ override fun writeClassInfo(output: SerializationOutput) {}
+
+ override fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? {
return obj
}
diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/Schema.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/Schema.kt
index 64a28a7aae..5c627cc943 100644
--- a/core/src/main/kotlin/net/corda/core/serialization/amqp/Schema.kt
+++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/Schema.kt
@@ -87,7 +87,7 @@ data class Schema(val types: List) : DescribedType {
override fun toString(): String = types.joinToString("\n")
}
-data class Descriptor(val name: String?, val code: UnsignedLong?) : DescribedType {
+data class Descriptor(val name: String?, val code: UnsignedLong? = null) : DescribedType {
companion object : DescribedTypeConstructor {
val DESCRIPTOR = UnsignedLong(3L or DESCRIPTOR_TOP_32BITS)
@@ -320,9 +320,9 @@ private val ANY_TYPE_HASH: String = "Any type = true"
* different.
*/
// TODO: write tests
-internal fun fingerprintForType(type: Type): String = Base58.encode(fingerprintForType(type, HashSet(), Hashing.murmur3_128().newHasher()).hash().asBytes())
+internal fun fingerprintForType(type: Type, factory: SerializerFactory): String = Base58.encode(fingerprintForType(type, HashSet(), Hashing.murmur3_128().newHasher(), factory).hash().asBytes())
-private fun fingerprintForType(type: Type, alreadySeen: MutableSet, hasher: Hasher): Hasher {
+private fun fingerprintForType(type: Type, alreadySeen: MutableSet, hasher: Hasher, factory: SerializerFactory): Hasher {
return if (type in alreadySeen) {
hasher.putUnencodedChars(ALREADY_SEEN_HASH)
} else {
@@ -331,25 +331,31 @@ private fun fingerprintForType(type: Type, alreadySeen: MutableSet, hasher
hasher.putUnencodedChars(ANY_TYPE_HASH)
} else if (type is Class<*>) {
if (type.isArray) {
- fingerprintForType(type.componentType, alreadySeen, hasher).putUnencodedChars(ARRAY_HASH)
+ fingerprintForType(type.componentType, alreadySeen, hasher, factory).putUnencodedChars(ARRAY_HASH)
} else if (SerializerFactory.isPrimitive(type)) {
hasher.putUnencodedChars(type.name)
} else if (Collection::class.java.isAssignableFrom(type) || Map::class.java.isAssignableFrom(type)) {
hasher.putUnencodedChars(type.name)
} else {
- // Hash the class + properties + interfaces
- propertiesForSerialization(constructorForDeserialization(type), type).fold(hasher.putUnencodedChars(type.name)) { orig, param ->
- fingerprintForType(param.readMethod.genericReturnType, alreadySeen, orig).putUnencodedChars(param.name).putUnencodedChars(if (param.mandatory) NOT_NULLABLE_HASH else NULLABLE_HASH)
+ // Need to check if a custom serializer is applicable
+ val customSerializer = factory.findCustomSerializer(type)
+ if (customSerializer == null) {
+ // Hash the class + properties + interfaces
+ propertiesForSerialization(constructorForDeserialization(type), type, factory).fold(hasher.putUnencodedChars(type.name)) { orig, param ->
+ fingerprintForType(param.readMethod.genericReturnType, alreadySeen, orig, factory).putUnencodedChars(param.name).putUnencodedChars(if (param.mandatory) NOT_NULLABLE_HASH else NULLABLE_HASH)
+ }
+ interfacesForSerialization(type).map { fingerprintForType(it, alreadySeen, hasher, factory) }
+ hasher
+ } else {
+ hasher.putUnencodedChars(customSerializer.typeDescriptor)
}
- interfacesForSerialization(type).map { fingerprintForType(it, alreadySeen, hasher) }
- hasher
}
} else if (type is ParameterizedType) {
// Hash the rawType + params
- type.actualTypeArguments.fold(fingerprintForType(type.rawType, alreadySeen, hasher)) { orig, paramType -> fingerprintForType(paramType, alreadySeen, orig) }
+ type.actualTypeArguments.fold(fingerprintForType(type.rawType, alreadySeen, hasher, factory)) { orig, paramType -> fingerprintForType(paramType, alreadySeen, orig, factory) }
} else if (type is GenericArrayType) {
// Hash the element type + some array hash
- fingerprintForType(type.genericComponentType, alreadySeen, hasher).putUnencodedChars(ARRAY_HASH)
+ fingerprintForType(type.genericComponentType, alreadySeen, hasher, factory).putUnencodedChars(ARRAY_HASH)
} else {
throw NotSerializableException("Don't know how to hash $type")
}
diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/SerializationHelper.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/SerializationHelper.kt
index 107769cde7..85082544a4 100644
--- a/core/src/main/kotlin/net/corda/core/serialization/amqp/SerializationHelper.kt
+++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/SerializationHelper.kt
@@ -1,14 +1,16 @@
package net.corda.core.serialization.amqp
+import com.google.common.reflect.TypeToken
import org.apache.qpid.proton.codec.Data
import java.beans.Introspector
-import java.beans.PropertyDescriptor
import java.io.NotSerializableException
+import java.lang.reflect.Method
import java.lang.reflect.Modifier
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
+import kotlin.reflect.KParameter
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.jvm.javaType
@@ -58,24 +60,26 @@ internal fun constructorForDeserialization(clazz: Class): KFunction
* Note, you will need any Java classes to be compiled with the `-parameters` option to ensure constructor parameters have
* names accessible via reflection.
*/
-internal fun propertiesForSerialization(kotlinConstructor: KFunction?, clazz: Class<*>): Collection {
- return if (kotlinConstructor != null) propertiesForSerialization(kotlinConstructor) else propertiesForSerialization(clazz)
+internal fun propertiesForSerialization(kotlinConstructor: KFunction?, clazz: Class<*>, factory: SerializerFactory): Collection {
+ return if (kotlinConstructor != null) propertiesForSerialization(kotlinConstructor, factory) else propertiesForSerialization(clazz, factory)
}
private fun isConcrete(clazz: Class<*>): Boolean = !(clazz.isInterface || Modifier.isAbstract(clazz.modifiers))
-private fun propertiesForSerialization(kotlinConstructor: KFunction): Collection {
+private fun propertiesForSerialization(kotlinConstructor: KFunction, factory: SerializerFactory): Collection {
val clazz = (kotlinConstructor.returnType.classifier as KClass<*>).javaObjectType
// Kotlin reflection doesn't work with Java getters the way you might expect, so we drop back to good ol' beans.
- val properties: Map = Introspector.getBeanInfo(clazz).propertyDescriptors.filter { it.name != "class" }.groupBy { it.name }.mapValues { it.value[0] }
+ val properties = Introspector.getBeanInfo(clazz).propertyDescriptors.filter { it.name != "class" }.groupBy { it.name }.mapValues { it.value[0] }
val rc: MutableList = ArrayList(kotlinConstructor.parameters.size)
for (param in kotlinConstructor.parameters) {
val name = param.name ?: throw NotSerializableException("Constructor parameter of $clazz has no name.")
- val matchingProperty = properties[name] ?: throw NotSerializableException("No property matching constructor parameter named $name of $clazz. If using Java, check that you have the -parameters option specified in the Java compiler.")
+ val matchingProperty = properties[name] ?: throw NotSerializableException("No property matching constructor parameter named $name of $clazz." +
+ " If using Java, check that you have the -parameters option specified in the Java compiler.")
// Check that the method has a getter in java.
- val getter = matchingProperty.readMethod ?: throw NotSerializableException("Property has no getter method for $name of $clazz. If using Java and the parameter name looks anonymous, check that you have the -parameters option specified in the Java compiler.")
- if (getter.genericReturnType == param.type.javaType) {
- rc += PropertySerializer.make(name, getter)
+ val getter = matchingProperty.readMethod ?: throw NotSerializableException("Property has no getter method for $name of $clazz." +
+ " If using Java and the parameter name looks anonymous, check that you have the -parameters option specified in the Java compiler.")
+ if (constructorParamTakesReturnTypeOfGetter(getter, param)) {
+ rc += PropertySerializer.make(name, getter, factory)
} else {
throw NotSerializableException("Property type ${getter.genericReturnType} for $name of $clazz differs from constructor parameter type ${param.type.javaType}")
}
@@ -83,14 +87,16 @@ private fun propertiesForSerialization(kotlinConstructor: KFunction
return rc
}
-private fun propertiesForSerialization(clazz: Class<*>): Collection {
+private fun constructorParamTakesReturnTypeOfGetter(getter: Method, param: KParameter): Boolean = TypeToken.of(param.type.javaType).isSupertypeOf(getter.genericReturnType)
+
+private fun propertiesForSerialization(clazz: Class<*>, factory: SerializerFactory): Collection {
// Kotlin reflection doesn't work with Java getters the way you might expect, so we drop back to good ol' beans.
val properties = Introspector.getBeanInfo(clazz).propertyDescriptors.filter { it.name != "class" }.sortedBy { it.name }
val rc: MutableList = ArrayList(properties.size)
for (property in properties) {
// Check that the method has a getter in java.
val getter = property.readMethod ?: throw NotSerializableException("Property has no getter method for ${property.name} of $clazz.")
- rc += PropertySerializer.make(property.name, getter)
+ rc += PropertySerializer.make(property.name, getter, factory)
}
return rc
}
@@ -104,6 +110,7 @@ internal fun interfacesForSerialization(clazz: Class<*>): List {
private fun exploreType(type: Type?, interfaces: MutableSet) {
val clazz = (type as? Class<*>) ?: (type as? ParameterizedType)?.rawType as? Class<*>
if (clazz != null) {
+ if (clazz.isInterface) interfaces += clazz
for (newInterface in clazz.genericInterfaces) {
if (newInterface !in interfaces) {
interfaces += newInterface
diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/SerializationOutput.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/SerializationOutput.kt
index f440d62c2a..3cbfad41ba 100644
--- a/core/src/main/kotlin/net/corda/core/serialization/amqp/SerializationOutput.kt
+++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/SerializationOutput.kt
@@ -14,10 +14,10 @@ import kotlin.collections.LinkedHashSet
* @param serializerFactory This is the factory for [AMQPSerializer] instances and can be shared across multiple
* instances and threads.
*/
-class SerializationOutput(private val serializerFactory: SerializerFactory = SerializerFactory()) {
+open class SerializationOutput(internal val serializerFactory: SerializerFactory = SerializerFactory()) {
// TODO: we're not supporting object refs yet
private val objectHistory: MutableMap = IdentityHashMap()
- private val serializerHistory: MutableSet = LinkedHashSet()
+ private val serializerHistory: MutableSet> = LinkedHashSet()
private val schemaHistory: MutableSet = LinkedHashSet()
/**
@@ -64,19 +64,21 @@ class SerializationOutput(private val serializerFactory: SerializerFactory = Ser
internal fun writeObject(obj: Any, data: Data, type: Type) {
val serializer = serializerFactory.get(obj.javaClass, type)
if (serializer !in serializerHistory) {
+ serializerHistory.add(serializer)
serializer.writeClassInfo(this)
}
serializer.writeObject(obj, data, type, this)
}
- internal fun writeTypeNotations(vararg typeNotation: TypeNotation): Boolean {
+ open internal fun writeTypeNotations(vararg typeNotation: TypeNotation): Boolean {
return schemaHistory.addAll(typeNotation)
}
- internal fun requireSerializer(type: Type) {
- if (type != SerializerFactory.AnyType) {
+ open internal fun requireSerializer(type: Type) {
+ if (type != SerializerFactory.AnyType && type != Object::class.java) {
val serializer = serializerFactory.get(null, type)
if (serializer !in serializerHistory) {
+ serializerHistory.add(serializer)
serializer.writeClassInfo(this)
}
}
diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/SerializerFactory.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/SerializerFactory.kt
index 1456c9a7ca..3883aad9dd 100644
--- a/core/src/main/kotlin/net/corda/core/serialization/amqp/SerializerFactory.kt
+++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/SerializerFactory.kt
@@ -10,18 +10,19 @@ import java.io.NotSerializableException
import java.lang.reflect.GenericArrayType
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
+import java.lang.reflect.WildcardType
import java.util.*
import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.CopyOnWriteArrayList
import javax.annotation.concurrent.ThreadSafe
/**
* Factory of serializers designed to be shared across threads and invocations.
*/
+// TODO: enums
// TODO: object references
// TODO: class references? (e.g. cheat with repeated descriptors using a long encoding, like object ref proposal)
// TODO: Inner classes etc
-// TODO: support for custom serialisation of core types (of e.g. PublicKey, Throwables)
-// TODO: exclude schemas for core types that don't need custom serializers that everyone already knows the schema for.
// TODO: support for intern-ing of deserialized objects for some core types (e.g. PublicKey) for memory efficiency
// TODO: maybe support for caching of serialized form of some core types for performance
// TODO: profile for performance in general
@@ -30,10 +31,13 @@ import javax.annotation.concurrent.ThreadSafe
// TODO: incorporate the class carpenter for classes not on the classpath.
// TODO: apply class loader logic and an "app context" throughout this code.
// TODO: schema evolution solution when the fingerprints do not line up.
+// TODO: allow definition of well known types that are left out of the schema.
+// TODO: automatically support byte[] without having to wrap in [Binary].
@ThreadSafe
class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) {
- private val serializersByType = ConcurrentHashMap()
- private val serializersByDescriptor = ConcurrentHashMap()
+ private val serializersByType = ConcurrentHashMap>()
+ private val serializersByDescriptor = ConcurrentHashMap>()
+ private val customSerializers = CopyOnWriteArrayList>()
/**
* Look up, and manufacture if necessary, a serializer for the given type.
@@ -42,7 +46,7 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) {
* restricted type processing).
*/
@Throws(NotSerializableException::class)
- fun get(actualType: Class<*>?, declaredType: Type): AMQPSerializer {
+ fun get(actualType: Class<*>?, declaredType: Type): AMQPSerializer {
if (declaredType is ParameterizedType) {
return serializersByType.computeIfAbsent(declaredType) {
// We allow only Collection and Map.
@@ -50,7 +54,7 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) {
if (rawType is Class<*>) {
checkParameterisedTypesConcrete(declaredType.actualTypeArguments)
if (Collection::class.java.isAssignableFrom(rawType)) {
- CollectionSerializer(declaredType)
+ CollectionSerializer(declaredType, this)
} else if (Map::class.java.isAssignableFrom(rawType)) {
makeMapSerializer(declaredType)
} else {
@@ -63,27 +67,44 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) {
} else if (declaredType is Class<*>) {
// Simple classes allowed
if (Collection::class.java.isAssignableFrom(declaredType)) {
- return serializersByType.computeIfAbsent(declaredType) { CollectionSerializer(DeserializedParameterizedType(declaredType, arrayOf(AnyType), null)) }
+ return serializersByType.computeIfAbsent(declaredType) { CollectionSerializer(DeserializedParameterizedType(declaredType, arrayOf(AnyType), null), this) }
} else if (Map::class.java.isAssignableFrom(declaredType)) {
return serializersByType.computeIfAbsent(declaredType) { makeMapSerializer(DeserializedParameterizedType(declaredType, arrayOf(AnyType, AnyType), null)) }
} else {
return makeClassSerializer(actualType ?: declaredType)
}
} else if (declaredType is GenericArrayType) {
- return serializersByType.computeIfAbsent(declaredType) { ArraySerializer(declaredType) }
+ return serializersByType.computeIfAbsent(declaredType) { ArraySerializer(declaredType, this) }
} else {
throw NotSerializableException("Declared types of $declaredType are not supported.")
}
}
+ /**
+ * Lookup and manufacture a serializer for the given AMQP type descriptor, assuming we also have the necessary types
+ * contained in the [Schema].
+ */
@Throws(NotSerializableException::class)
- fun get(typeDescriptor: Any, envelope: Envelope): AMQPSerializer {
+ fun get(typeDescriptor: Any, schema: Schema): AMQPSerializer {
return serializersByDescriptor[typeDescriptor] ?: {
- processSchema(envelope.schema)
+ processSchema(schema)
serializersByDescriptor[typeDescriptor] ?: throw NotSerializableException("Could not find type matching descriptor $typeDescriptor.")
}()
}
+ /**
+ * TODO: Add docs
+ */
+ fun register(customSerializer: CustomSerializer) {
+ if (!serializersByDescriptor.containsKey(customSerializer.typeDescriptor)) {
+ customSerializers += customSerializer
+ serializersByDescriptor[customSerializer.typeDescriptor] = customSerializer
+ for (additional in customSerializer.additionalSerializers) {
+ register(additional)
+ }
+ }
+ }
+
private fun processSchema(schema: Schema) {
for (typeNotation in schema.types) {
processSchemaEntry(typeNotation)
@@ -99,7 +120,14 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) {
private fun restrictedTypeForName(name: String): Type {
return if (name.endsWith("[]")) {
- DeserializedGenericArrayType(restrictedTypeForName(name.substring(0, name.lastIndex - 1)))
+ val elementType = restrictedTypeForName(name.substring(0, name.lastIndex - 1))
+ if (elementType is ParameterizedType || elementType is GenericArrayType) {
+ DeserializedGenericArrayType(elementType)
+ } else if (elementType is Class<*>) {
+ java.lang.reflect.Array.newInstance(elementType, 0).javaClass
+ } else {
+ throw NotSerializableException("Not able to deserialize array type: $name")
+ }
} else {
DeserializedParameterizedType.make(name)
}
@@ -134,32 +162,52 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) {
}
}
- private fun makeClassSerializer(clazz: Class<*>): AMQPSerializer {
+ private fun makeClassSerializer(clazz: Class<*>): AMQPSerializer {
return serializersByType.computeIfAbsent(clazz) {
- if (clazz.isArray) {
- whitelisted(clazz.componentType)
- ArraySerializer(clazz)
- } else if (isPrimitive(clazz)) {
+ if (isPrimitive(clazz)) {
AMQPPrimitiveSerializer(clazz)
} else {
- whitelisted(clazz)
- ObjectSerializer(clazz)
+ findCustomSerializer(clazz) ?: {
+ if (clazz.isArray) {
+ whitelisted(clazz.componentType)
+ ArraySerializer(clazz, this)
+ } else {
+ whitelisted(clazz)
+ ObjectSerializer(clazz, this)
+ }
+ }()
}
}
}
+ internal fun findCustomSerializer(clazz: Class<*>): AMQPSerializer? {
+ for (customSerializer in customSerializers) {
+ if (customSerializer.isSerializerFor(clazz)) {
+ return customSerializer
+ }
+ }
+ return null
+ }
+
private fun whitelisted(clazz: Class<*>): Boolean {
- if (whitelist.hasListed(clazz) || clazz.isAnnotationPresent(CordaSerializable::class.java)) {
+ if (whitelist.hasListed(clazz) || hasAnnotationInHierarchy(clazz)) {
return true
} else {
throw NotSerializableException("Class $clazz is not on the whitelist or annotated with @CordaSerializable.")
}
}
- private fun makeMapSerializer(declaredType: ParameterizedType): AMQPSerializer {
+ // Recursively check the class, interfaces and superclasses for our annotation.
+ internal fun hasAnnotationInHierarchy(type: Class<*>): Boolean {
+ return type.isAnnotationPresent(CordaSerializable::class.java) ||
+ type.interfaces.any { it.isAnnotationPresent(CordaSerializable::class.java) || hasAnnotationInHierarchy(it) }
+ || (type.superclass != null && hasAnnotationInHierarchy(type.superclass))
+ }
+
+ private fun makeMapSerializer(declaredType: ParameterizedType): AMQPSerializer {
val rawType = declaredType.rawType as Class<*>
rawType.checkNotUnorderedHashMap()
- return MapSerializer(declaredType)
+ return MapSerializer(declaredType, this)
}
companion object {
@@ -185,12 +233,17 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) {
Char::class.java to "char",
Date::class.java to "timestamp",
UUID::class.java to "uuid",
- ByteArray::class.java to "binary",
+ Binary::class.java to "binary",
String::class.java to "string",
Symbol::class.java to "symbol")
}
- object AnyType : Type {
- override fun toString(): String = "*"
+ object AnyType : WildcardType {
+ override fun getUpperBounds(): Array = arrayOf(Object::class.java)
+
+ override fun getLowerBounds(): Array = emptyArray()
+
+ override fun toString(): String = "?"
}
}
+
diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/PublicKeySerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/PublicKeySerializer.kt
new file mode 100644
index 0000000000..46536a1bed
--- /dev/null
+++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/PublicKeySerializer.kt
@@ -0,0 +1,24 @@
+package net.corda.core.serialization.amqp.custom
+
+import net.corda.core.crypto.Crypto
+import net.corda.core.serialization.amqp.*
+import org.apache.qpid.proton.amqp.Binary
+import org.apache.qpid.proton.codec.Data
+import java.lang.reflect.Type
+import java.security.PublicKey
+
+class PublicKeySerializer : CustomSerializer.Implements(PublicKey::class.java) {
+ override val additionalSerializers: Iterable> = emptyList()
+
+ override val schemaForDocumentation = Schema(listOf(RestrictedType(type.toString(), "", listOf(type.toString()), SerializerFactory.primitiveTypeName(Binary::class.java)!!, descriptor, emptyList())))
+
+ override fun writeDescribedObject(obj: PublicKey, data: Data, type: Type, output: SerializationOutput) {
+ // TODO: Instead of encoding to the default X509 format, we could have a custom per key type (space-efficient) serialiser.
+ output.writeObject(Binary(obj.encoded), data, clazz)
+ }
+
+ override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): PublicKey {
+ val A = input.readObject(obj, schema, ByteArray::class.java) as Binary
+ return Crypto.decodePublicKey(A.array)
+ }
+}
\ No newline at end of file
diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/ThrowableSerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/ThrowableSerializer.kt
new file mode 100644
index 0000000000..ed267ed44d
--- /dev/null
+++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/ThrowableSerializer.kt
@@ -0,0 +1,81 @@
+package net.corda.core.serialization.amqp.custom
+
+import net.corda.core.serialization.amqp.CustomSerializer
+import net.corda.core.serialization.amqp.SerializerFactory
+import net.corda.core.serialization.amqp.constructorForDeserialization
+import net.corda.core.serialization.amqp.propertiesForSerialization
+import net.corda.core.utilities.CordaRuntimeException
+import net.corda.core.utilities.CordaThrowable
+import java.io.NotSerializableException
+
+class ThrowableSerializer(factory: SerializerFactory) : CustomSerializer.Proxy(Throwable::class.java, ThrowableProxy::class.java, factory) {
+ override val additionalSerializers: Iterable> = listOf(StackTraceElementSerializer(factory))
+
+ override fun toProxy(obj: Throwable): ThrowableProxy {
+ val extraProperties: MutableMap = LinkedHashMap()
+ val message = if (obj is CordaThrowable) {
+ // Try and find a constructor
+ try {
+ val constructor = constructorForDeserialization(obj.javaClass)
+ val props = propertiesForSerialization(constructor, obj.javaClass, factory)
+ for (prop in props) {
+ extraProperties[prop.name] = prop.readMethod.invoke(obj)
+ }
+ } catch(e: NotSerializableException) {
+ }
+ obj.originalMessage
+ } else {
+ obj.message
+ }
+ return ThrowableProxy(obj.javaClass.name, message, obj.stackTrace, obj.cause, obj.suppressed, extraProperties)
+ }
+
+ override fun fromProxy(proxy: ThrowableProxy): Throwable {
+ try {
+ // TODO: This will need reworking when we have multiple class loaders
+ val clazz = Class.forName(proxy.exceptionClass, false, this.javaClass.classLoader)
+ // If it is CordaException or CordaRuntimeException, we can seek any constructor and then set the properties
+ // Otherwise we just make a CordaRuntimeException
+ if (CordaThrowable::class.java.isAssignableFrom(clazz) && Throwable::class.java.isAssignableFrom(clazz)) {
+ val constructor = constructorForDeserialization(clazz)!!
+ val throwable = constructor.callBy(constructor.parameters.map { it to proxy.additionalProperties[it.name] }.toMap())
+ (throwable as CordaThrowable).apply {
+ if (this.javaClass.name != proxy.exceptionClass) this.originalExceptionClassName = proxy.exceptionClass
+ this.setMessage(proxy.message)
+ this.setCause(proxy.cause)
+ this.addSuppressed(proxy.suppressed)
+ }
+ return (throwable as Throwable).apply {
+ this.stackTrace = proxy.stackTrace
+ }
+ }
+ } catch (e: Exception) {
+ // If attempts to rebuild the exact exception fail, we fall through and build a runtime exception.
+ }
+ // If the criteria are not met or we experience an exception constructing the exception, we fall back to our own unchecked exception.
+ return CordaRuntimeException(proxy.exceptionClass).apply {
+ this.setMessage(proxy.message)
+ this.setCause(proxy.cause)
+ this.stackTrace = proxy.stackTrace
+ this.addSuppressed(proxy.suppressed)
+ }
+ }
+
+ class ThrowableProxy(
+ val exceptionClass: String,
+ val message: String?,
+ val stackTrace: Array,
+ val cause: Throwable?,
+ val suppressed: Array,
+ val additionalProperties: Map)
+}
+
+class StackTraceElementSerializer(factory: SerializerFactory) : CustomSerializer.Proxy(StackTraceElement::class.java, StackTraceElementProxy::class.java, factory) {
+ override val additionalSerializers: Iterable> = emptyList()
+
+ override fun toProxy(obj: StackTraceElement): StackTraceElementProxy = StackTraceElementProxy(obj.className, obj.methodName, obj.fileName, obj.lineNumber)
+
+ override fun fromProxy(proxy: StackTraceElementProxy): StackTraceElement = StackTraceElement(proxy.declaringClass, proxy.methodName, proxy.fileName, proxy.lineNumber)
+
+ data class StackTraceElementProxy(val declaringClass: String, val methodName: String, val fileName: String?, val lineNumber: Int)
+}
\ No newline at end of file
diff --git a/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt
index 61010282e8..876efdda82 100644
--- a/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt
+++ b/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt
@@ -36,12 +36,12 @@ abstract class BaseTransaction(
* If specified, a time window in which this transaction may have been notarised. Contracts can check this
* time window to find out when a transaction is deemed to have occurred, from the ledger's perspective.
*/
- val timestamp: Timestamp?
+ val timeWindow: TimeWindow?
) : NamedByHash {
protected fun checkInvariants() {
- if (notary == null) check(inputs.isEmpty()) { "The notary must be specified explicitly for any transaction that has inputs." }
- if (timestamp != null) check(notary != null) { "If a timestamp is provided, there must be a notary." }
+ if (notary == null) check(inputs.isEmpty()) { "The notary must be specified explicitly for any transaction that has inputs" }
+ if (timeWindow != null) check(notary != null) { "If a time-window is provided, there must be a notary" }
}
override fun equals(other: Any?): Boolean {
@@ -50,10 +50,10 @@ abstract class BaseTransaction(
notary == other.notary &&
mustSign == other.mustSign &&
type == other.type &&
- timestamp == other.timestamp
+ timeWindow == other.timeWindow
}
- override fun hashCode() = Objects.hash(notary, mustSign, type, timestamp)
+ override fun hashCode() = Objects.hash(notary, mustSign, type, timeWindow)
override fun toString(): String = "${javaClass.simpleName}(id=$id)"
}
diff --git a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt
index 4665387c7d..3fd91f1f0e 100644
--- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt
+++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt
@@ -32,9 +32,9 @@ class LedgerTransaction(
override val id: SecureHash,
notary: Party?,
signers: List,
- timestamp: Timestamp?,
+ timeWindow: TimeWindow?,
type: TransactionType
-) : BaseTransaction(inputs, outputs, notary, signers, type, timestamp) {
+) : BaseTransaction(inputs, outputs, notary, signers, type, timeWindow) {
init {
checkInvariants()
}
@@ -47,7 +47,7 @@ class LedgerTransaction(
/** Strips the transaction down to a form that is usable by the contract verify functions */
fun toTransactionForContract(): TransactionForContract {
return TransactionForContract(inputs.map { it.state.data }, outputs.map { it.data }, attachments, commands, id,
- inputs.map { it.state.notary }.singleOrNull(), timestamp)
+ inputs.map { it.state.notary }.singleOrNull(), timeWindow)
}
/**
diff --git a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt
index 78c25f7154..0e611fb242 100644
--- a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt
+++ b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt
@@ -11,6 +11,7 @@ import net.corda.core.serialization.p2PKryo
import net.corda.core.serialization.serialize
import net.corda.core.serialization.withoutReferences
import java.security.PublicKey
+import java.util.function.Predicate
fun serializedHash(x: T): SecureHash {
return p2PKryo().run { kryo -> kryo.withoutReferences { x.serialize(kryo).hash } }
@@ -33,7 +34,7 @@ interface TraversableTransaction {
val notary: Party?
val mustSign: List
val type: TransactionType?
- val timestamp: Timestamp?
+ val timeWindow: TimeWindow?
/**
* Returns a flattened list of all the components that are present in the transaction, in the following order:
@@ -45,7 +46,7 @@ interface TraversableTransaction {
* - The notary [Party], if present
* - Each required signer ([mustSign]) that is present
* - The type of the transaction, if present
- * - The timestamp of the transaction, if present
+ * - The time-window of the transaction, if present
*/
val availableComponents: List
get() {
@@ -56,7 +57,7 @@ interface TraversableTransaction {
notary?.let { result += it }
result.addAll(mustSign)
type?.let { result += it }
- timestamp?.let { result += it }
+ timeWindow?.let { result += it }
return result
}
@@ -81,7 +82,7 @@ class FilteredLeaves(
override val notary: Party?,
override val mustSign: List,
override val type: TransactionType?,
- override val timestamp: Timestamp?
+ override val timeWindow: TimeWindow?
) : TraversableTransaction {
/**
* Function that checks the whole filtered structure.
@@ -116,8 +117,9 @@ class FilteredTransaction private constructor(
* @param wtx WireTransaction to be filtered.
* @param filtering filtering over the whole WireTransaction
*/
+ @JvmStatic
fun buildMerkleTransaction(wtx: WireTransaction,
- filtering: (Any) -> Boolean
+ filtering: Predicate
): FilteredTransaction {
val filteredLeaves = wtx.filterWithFun(filtering)
val merkleTree = wtx.merkleTree
diff --git a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt
index e100c89793..00f83fdf3e 100644
--- a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt
+++ b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt
@@ -26,9 +26,11 @@ import java.util.*
* when verifying composite key signatures, but may be used as individual signatures where a single key is expected to
* sign.
*/
+// DOCSTART 1
data class SignedTransaction(val txBits: SerializedBytes,
val sigs: List
) : NamedByHash {
+// DOCEND 1
init {
require(sigs.isNotEmpty())
}
@@ -64,8 +66,10 @@ data class SignedTransaction(val txBits: SerializedBytes,
* @throws SignatureException if any signatures are invalid or unrecognised.
* @throws SignaturesMissingException if any signatures should have been present but were not.
*/
+ // DOCSTART 2
@Throws(SignatureException::class)
fun verifySignatures(vararg allowedToBeMissing: PublicKey): WireTransaction {
+ // DOCEND 2
// Embedded WireTransaction is not deserialised until after we check the signatures.
checkSignaturesAreValid()
diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt
index 19396b1e01..e727ad7c7c 100644
--- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt
+++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt
@@ -3,7 +3,7 @@ package net.corda.core.transactions
import co.paralleluniverse.strands.Strand
import net.corda.core.contracts.*
import net.corda.core.crypto.*
-import net.corda.core.flows.FlowStateMachine
+import net.corda.core.internal.FlowStateMachine
import net.corda.core.identity.Party
import net.corda.core.serialization.serialize
import java.security.KeyPair
@@ -37,9 +37,10 @@ open class TransactionBuilder(
protected val outputs: MutableList> = arrayListOf(),
protected val commands: MutableList = arrayListOf(),
protected val signers: MutableSet = mutableSetOf(),
- protected var timestamp: Timestamp? = null) {
+ protected var timeWindow: TimeWindow? = null) {
+ constructor(type: TransactionType, notary: Party) : this(type, notary, (Strand.currentStrand() as? FlowStateMachine<*>)?.id?.uuid ?: UUID.randomUUID())
- val time: Timestamp? get() = timestamp
+ val time: TimeWindow? get() = timeWindow // TODO: rename using a more descriptive name (i.e. timeWindowGetter) or remove if unused.
/**
* Creates a copy of the builder.
@@ -53,30 +54,31 @@ open class TransactionBuilder(
outputs = ArrayList(outputs),
commands = ArrayList(commands),
signers = LinkedHashSet(signers),
- timestamp = timestamp
+ timeWindow = timeWindow
)
/**
- * Places a [TimestampCommand] in this transaction, removing any existing command if there is one.
+ * Places a [TimeWindow] in this transaction, removing any existing command if there is one.
* The command requires a signature from the Notary service, which acts as a Timestamp Authority.
* The signature can be obtained using [NotaryFlow].
*
- * The window of time in which the final timestamp may lie is defined as [time] +/- [timeTolerance].
+ * The window of time in which the final time-window may lie is defined as [time] +/- [timeTolerance].
* If you want a non-symmetrical time window you must add the command via [addCommand] yourself. The tolerance
* should be chosen such that your code can finish building the transaction and sending it to the TSA within that
* window of time, taking into account factors such as network latency. Transactions being built by a group of
* collaborating parties may therefore require a higher time tolerance than a transaction being built by a single
* node.
*/
- fun setTime(time: Instant, timeTolerance: Duration) = setTime(Timestamp(time, timeTolerance))
+ fun addTimeWindow(time: Instant, timeTolerance: Duration) = addTimeWindow(TimeWindow.withTolerance(time, timeTolerance))
- fun setTime(newTimestamp: Timestamp) {
- check(notary != null) { "Only notarised transactions can have a timestamp" }
+ fun addTimeWindow(timeWindow: TimeWindow) {
+ check(notary != null) { "Only notarised transactions can have a time-window" }
signers.add(notary!!.owningKey)
- check(currentSigs.isEmpty()) { "Cannot change timestamp after signing" }
- this.timestamp = newTimestamp
+ check(currentSigs.isEmpty()) { "Cannot change time-window after signing" }
+ this.timeWindow = timeWindow
}
+ // DOCSTART 1
/** A more convenient way to add items to this transaction that calls the add* methods for you based on type */
fun withItems(vararg items: Any): TransactionBuilder {
for (t in items) {
@@ -91,10 +93,12 @@ open class TransactionBuilder(
}
return this
}
+ // DOCEND 1
/** The signatures that have been collected so far - might be incomplete! */
protected val currentSigs = arrayListOf()
+ @Deprecated("Use ServiceHub.signInitialTransaction() instead.")
fun signWith(key: KeyPair): TransactionBuilder {
check(currentSigs.none { it.by == key.public }) { "This partial transaction was already signed by ${key.public}" }
val data = toWireTransaction().id
@@ -132,7 +136,7 @@ open class TransactionBuilder(
}
fun toWireTransaction() = WireTransaction(ArrayList(inputs), ArrayList(attachments),
- ArrayList(outputs), ArrayList(commands), notary, signers.toList(), type, timestamp)
+ ArrayList(outputs), ArrayList(commands), notary, signers.toList(), type, timeWindow)
fun toSignedTransaction(checkSufficientSignatures: Boolean = true): SignedTransaction {
if (checkSufficientSignatures) {
diff --git a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt
index 09e2b9218b..ecc2c58be9 100644
--- a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt
+++ b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt
@@ -13,6 +13,7 @@ import net.corda.core.serialization.p2PKryo
import net.corda.core.serialization.serialize
import net.corda.core.utilities.Emoji
import java.security.PublicKey
+import java.util.function.Predicate
/**
* A transaction ready for serialisation, without any signatures attached. A WireTransaction is usually wrapped
@@ -31,8 +32,8 @@ class WireTransaction(
notary: Party?,
signers: List,
type: TransactionType,
- timestamp: Timestamp?
-) : BaseTransaction(inputs, outputs, notary, signers, type, timestamp), TraversableTransaction {
+ timeWindow: TimeWindow?
+) : BaseTransaction(inputs, outputs, notary, signers, type, timeWindow), TraversableTransaction {
init {
checkInvariants()
}
@@ -100,13 +101,13 @@ class WireTransaction(
val resolvedInputs = inputs.map { ref ->
resolveStateRef(ref)?.let { StateAndRef(it, ref) } ?: throw TransactionResolutionException(ref.txhash)
}
- return LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, notary, mustSign, timestamp, type)
+ return LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, notary, mustSign, timeWindow, type)
}
/**
* Build filtered transaction using provided filtering functions.
*/
- fun buildFilteredTransaction(filtering: (Any) -> Boolean): FilteredTransaction {
+ fun buildFilteredTransaction(filtering: Predicate): FilteredTransaction {
return FilteredTransaction.buildMerkleTransaction(this, filtering)
}
@@ -120,17 +121,17 @@ class WireTransaction(
* @param filtering filtering over the whole WireTransaction
* @returns FilteredLeaves used in PartialMerkleTree calculation and verification.
*/
- fun filterWithFun(filtering: (Any) -> Boolean): FilteredLeaves {
- fun notNullFalse(elem: Any?): Any? = if (elem == null || !filtering(elem)) null else elem
+ fun filterWithFun(filtering: Predicate): FilteredLeaves {
+ fun notNullFalse(elem: Any?): Any? = if (elem == null || !filtering.test(elem)) null else elem
return FilteredLeaves(
- inputs.filter { filtering(it) },
- attachments.filter { filtering(it) },
- outputs.filter { filtering(it) },
- commands.filter { filtering(it) },
+ inputs.filter { filtering.test(it) },
+ attachments.filter { filtering.test(it) },
+ outputs.filter { filtering.test(it) },
+ commands.filter { filtering.test(it) },
notNullFalse(notary) as Party?,
- mustSign.filter { filtering(it) },
+ mustSign.filter { filtering.test(it) },
notNullFalse(type) as TransactionType?,
- notNullFalse(timestamp) as Timestamp?
+ notNullFalse(timeWindow) as TimeWindow?
)
}
diff --git a/core/src/main/kotlin/net/corda/core/utilities/CordaException.kt b/core/src/main/kotlin/net/corda/core/utilities/CordaException.kt
new file mode 100644
index 0000000000..907bbee408
--- /dev/null
+++ b/core/src/main/kotlin/net/corda/core/utilities/CordaException.kt
@@ -0,0 +1,103 @@
+package net.corda.core.utilities
+
+import net.corda.core.serialization.CordaSerializable
+import java.util.*
+
+@CordaSerializable
+interface CordaThrowable {
+ var originalExceptionClassName: String?
+ val originalMessage: String?
+ fun setMessage(message: String?)
+ fun setCause(cause: Throwable?)
+ fun addSuppressed(suppressed: Array)
+}
+
+open class CordaException internal constructor(override var originalExceptionClassName: String? = null,
+ private var _message: String? = null,
+ private var _cause: Throwable? = null) : Exception(null, null, true, true), CordaThrowable {
+
+ constructor(message: String?,
+ cause: Throwable?) : this(null, message, cause)
+
+ override val message: String?
+ get() = if (originalExceptionClassName == null) originalMessage else {
+ if (originalMessage == null) "$originalExceptionClassName" else "$originalExceptionClassName: $originalMessage"
+ }
+
+ override val cause: Throwable?
+ get() = _cause ?: super.cause
+
+ override fun setMessage(message: String?) {
+ _message = message
+ }
+
+ override fun setCause(cause: Throwable?) {
+ _cause = cause
+ }
+
+ override fun addSuppressed(suppressed: Array) {
+ for (suppress in suppressed) {
+ addSuppressed(suppress)
+ }
+ }
+
+ override val originalMessage: String?
+ get() = _message
+
+ override fun hashCode(): Int {
+ return Arrays.deepHashCode(stackTrace) xor Objects.hash(originalExceptionClassName, originalMessage)
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return other is CordaException &&
+ originalExceptionClassName == other.originalExceptionClassName &&
+ message == other.message &&
+ cause == other.cause &&
+ Arrays.equals(stackTrace, other.stackTrace) &&
+ Arrays.equals(suppressed, other.suppressed)
+ }
+}
+
+open class CordaRuntimeException internal constructor(override var originalExceptionClassName: String?,
+ private var _message: String? = null,
+ private var _cause: Throwable? = null) : RuntimeException(null, null, true, true), CordaThrowable {
+ constructor(message: String?, cause: Throwable?) : this(null, message, cause)
+
+ override val message: String?
+ get() = if (originalExceptionClassName == null) originalMessage else {
+ if (originalMessage == null) "$originalExceptionClassName" else "$originalExceptionClassName: $originalMessage"
+ }
+
+ override val cause: Throwable?
+ get() = _cause ?: super.cause
+
+ override fun setMessage(message: String?) {
+ _message = message
+ }
+
+ override fun setCause(cause: Throwable?) {
+ _cause = cause
+ }
+
+ override fun addSuppressed(suppressed: Array) {
+ for (suppress in suppressed) {
+ addSuppressed(suppress)
+ }
+ }
+
+ override val originalMessage: String?
+ get() = _message
+
+ override fun hashCode(): Int {
+ return Arrays.deepHashCode(stackTrace) xor Objects.hash(originalExceptionClassName, originalMessage)
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return other is CordaRuntimeException &&
+ originalExceptionClassName == other.originalExceptionClassName &&
+ message == other.message &&
+ cause == other.cause &&
+ Arrays.equals(stackTrace, other.stackTrace) &&
+ Arrays.equals(suppressed, other.suppressed)
+ }
+}
\ No newline at end of file
diff --git a/core/src/main/kotlin/net/corda/core/utilities/Emoji.kt b/core/src/main/kotlin/net/corda/core/utilities/Emoji.kt
index c51aa1f46e..bc7fd86a14 100644
--- a/core/src/main/kotlin/net/corda/core/utilities/Emoji.kt
+++ b/core/src/main/kotlin/net/corda/core/utilities/Emoji.kt
@@ -7,8 +7,12 @@ import net.corda.core.codePointsString
*/
object Emoji {
// Unfortunately only Apple has a terminal that can do colour emoji AND an emoji font installed by default.
+ // However the JediTerm java terminal emulator can also do emoji on OS X when using the JetBrains JRE.
+ // Check for that here. DemoBench sets TERM_PROGRAM appropriately.
val hasEmojiTerminal by lazy {
- System.getenv("CORDA_FORCE_EMOJI") != null || System.getenv("TERM_PROGRAM") in listOf("Apple_Terminal", "iTerm.app")
+ System.getenv("CORDA_FORCE_EMOJI") != null ||
+ System.getenv("TERM_PROGRAM") in listOf("Apple_Terminal", "iTerm.app") ||
+ (System.getenv("TERM_PROGRAM") == "JediTerm" && System.getProperty("java.vendor") == "JetBrains s.r.o")
}
@JvmStatic val CODE_SANTA_CLAUS: String = codePointsString(0x1F385)
diff --git a/core/src/main/kotlin/net/corda/core/utilities/ProcessUtilities.kt b/core/src/main/kotlin/net/corda/core/utilities/ProcessUtilities.kt
index 308824def6..d69ab38367 100644
--- a/core/src/main/kotlin/net/corda/core/utilities/ProcessUtilities.kt
+++ b/core/src/main/kotlin/net/corda/core/utilities/ProcessUtilities.kt
@@ -2,21 +2,24 @@ package net.corda.core.utilities
import java.nio.file.Path
+// TODO This doesn't belong in core and can be moved into node
object ProcessUtilities {
inline fun startJavaProcess(
arguments: List,
+ classpath: String = defaultClassPath,
jdwpPort: Int? = null,
extraJvmArguments: List = emptyList(),
inheritIO: Boolean = true,
errorLogPath: Path? = null,
workingDirectory: Path? = null
): Process {
- return startJavaProcess(C::class.java.name, arguments, jdwpPort, extraJvmArguments, inheritIO, errorLogPath, workingDirectory)
+ return startJavaProcess(C::class.java.name, arguments, classpath, jdwpPort, extraJvmArguments, inheritIO, errorLogPath, workingDirectory)
}
fun startJavaProcess(
className: String,
arguments: List,
+ classpath: String = defaultClassPath,
jdwpPort: Int? = null,
extraJvmArguments: List = emptyList(),
inheritIO: Boolean = true,
@@ -24,7 +27,6 @@ object ProcessUtilities {
workingDirectory: Path? = null
): Process {
val separator = System.getProperty("file.separator")
- val classpath = System.getProperty("java.class.path")
val javaPath = System.getProperty("java.home") + separator + "bin" + separator + "java"
val debugPortArgument = if (jdwpPort != null) {
listOf("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$jdwpPort")
@@ -44,4 +46,6 @@ object ProcessUtilities {
if (workingDirectory != null) directory(workingDirectory.toFile())
}.start()
}
+
+ val defaultClassPath: String get() = System.getProperty("java.class.path")
}
diff --git a/core/src/main/kotlin/net/corda/core/utilities/TestConstants.kt b/core/src/main/kotlin/net/corda/core/utilities/TestConstants.kt
index 1084808a4a..6861fa7fc0 100644
--- a/core/src/main/kotlin/net/corda/core/utilities/TestConstants.kt
+++ b/core/src/main/kotlin/net/corda/core/utilities/TestConstants.kt
@@ -4,10 +4,12 @@ package net.corda.core.utilities
import net.corda.core.crypto.*
import net.corda.core.identity.Party
+import net.corda.core.identity.PartyAndCertificate
import org.bouncycastle.asn1.x500.X500Name
import java.math.BigInteger
import java.security.KeyPair
import java.security.PublicKey
+import java.security.cert.CertificateFactory
import java.time.Instant
// A dummy time at which we will be pretending test transactions are created.
@@ -21,6 +23,7 @@ val DUMMY_KEY_2: KeyPair by lazy { generateKeyPair() }
val DUMMY_NOTARY_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(20)) }
/** Dummy notary identity for tests and simulations */
+val DUMMY_NOTARY_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(DUMMY_NOTARY)
val DUMMY_NOTARY: Party get() = Party(X500Name("CN=Notary Service,O=R3,OU=corda,L=Zurich,C=CH"), DUMMY_NOTARY_KEY.public)
val DUMMY_MAP_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(30)) }
@@ -29,7 +32,7 @@ val DUMMY_MAP: Party get() = Party(X500Name("CN=Network Map Service,O=R3,OU=cord
val DUMMY_BANK_A_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(40)) }
/** Dummy bank identity for tests and simulations */
-val DUMMY_BANK_A: Party get() = Party(X500Name("CN=Bank A,O=Bank A,L=London,C=UK"), DUMMY_BANK_A_KEY.public)
+val DUMMY_BANK_A: Party get() = Party(X500Name("CN=Bank A,O=Bank A,L=London,C=GB"), DUMMY_BANK_A_KEY.public)
val DUMMY_BANK_B_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(50)) }
/** Dummy bank identity for tests and simulations */
@@ -41,16 +44,37 @@ val DUMMY_BANK_C: Party get() = Party(X500Name("CN=Bank C,O=Bank C,L=Tokyo,C=JP"
val ALICE_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(70)) }
/** Dummy individual identity for tests and simulations */
-val ALICE: Party get() = Party(X500Name("CN=Alice Corp,O=Alice Corp,L=London,C=UK"), ALICE_KEY.public)
+val ALICE_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(ALICE)
+val ALICE: Party get() = Party(X500Name("CN=Alice Corp,O=Alice Corp,L=Madrid,C=ES"), ALICE_KEY.public)
val BOB_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(80)) }
/** Dummy individual identity for tests and simulations */
-val BOB: Party get() = Party(X500Name("CN=Bob Plc,O=Bob Plc,L=London,C=UK"), BOB_KEY.public)
+val BOB_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(BOB)
+val BOB: Party get() = Party(X500Name("CN=Bob Plc,O=Bob Plc,L=Rome,C=IT"), BOB_KEY.public)
val CHARLIE_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(90)) }
/** Dummy individual identity for tests and simulations */
-val CHARLIE: Party get() = Party(X500Name("CN=Charlie Ltd,O=Charlie Ltd,L=London,C=UK"), CHARLIE_KEY.public)
+val CHARLIE: Party get() = Party(X500Name("CN=Charlie Ltd,O=Charlie Ltd,L=Athens,C=GR"), CHARLIE_KEY.public)
val DUMMY_REGULATOR_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(100)) }
/** Dummy regulator for tests and simulations */
val DUMMY_REGULATOR: Party get() = Party(X500Name("CN=Regulator A,OU=Corda,O=AMF,L=Paris,C=FR"), DUMMY_REGULATOR_KEY.public)
+
+val DUMMY_CA_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(110)) }
+val DUMMY_CA: CertificateAndKeyPair by lazy {
+ // TODO: Should be identity scheme
+ val cert = X509Utilities.createSelfSignedCACertificate(X500Name("CN=Dummy CA,OU=Corda,O=R3 Ltd,L=London,C=GB"), DUMMY_CA_KEY)
+ CertificateAndKeyPair(cert, DUMMY_CA_KEY)
+}
+
+/**
+ * Build a test party with a nonsense certificate authority for testing purposes.
+ */
+fun getTestPartyAndCertificate(name: X500Name, publicKey: PublicKey, trustRoot: CertificateAndKeyPair = DUMMY_CA) = getTestPartyAndCertificate(Party(name, publicKey), trustRoot)
+
+fun getTestPartyAndCertificate(party: Party, trustRoot: CertificateAndKeyPair = DUMMY_CA): PartyAndCertificate {
+ val certFactory = CertificateFactory.getInstance("X509")
+ val certHolder = X509Utilities.createCertificate(CertificateType.IDENTITY, trustRoot.certificate, trustRoot.keyPair, party.name, party.owningKey)
+ val certPath = certFactory.generateCertPath(listOf(certHolder.cert, trustRoot.certificate.cert))
+ return PartyAndCertificate(party, certHolder, certPath)
+}
diff --git a/core/src/main/kotlin/net/corda/core/utilities/TimeWindow.kt b/core/src/main/kotlin/net/corda/core/utilities/TimeWindow.kt
deleted file mode 100644
index 2fb80ee61d..0000000000
--- a/core/src/main/kotlin/net/corda/core/utilities/TimeWindow.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package net.corda.core.utilities
-
-import java.time.Duration
-import java.time.Instant
-
-/**
- * A class representing a window in time from a particular instant, lasting a specified duration.
- */
-data class TimeWindow(val start: Instant, val duration: Duration) {
- val end: Instant
- get() = start + duration
-}
diff --git a/core/src/main/kotlin/net/corda/flows/CollectSignaturesFlow.kt b/core/src/main/kotlin/net/corda/flows/CollectSignaturesFlow.kt
index acfbba8541..5bd80d9f98 100644
--- a/core/src/main/kotlin/net/corda/flows/CollectSignaturesFlow.kt
+++ b/core/src/main/kotlin/net/corda/flows/CollectSignaturesFlow.kt
@@ -60,7 +60,6 @@ import java.security.PublicKey
* @param partiallySignedTx Transaction to collect the remaining signatures for
*/
// TODO: AbstractStateReplacementFlow needs updating to use this flow.
-// TODO: TwoPartyTradeFlow needs updating to use this flow.
// TODO: Update this flow to handle randomly generated keys when that works is complete.
class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction,
override val progressTracker: ProgressTracker = tracker()): FlowLogic() {
@@ -123,6 +122,7 @@ class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction,
partyNode.legalIdentity
}
+ // DOCSTART 1
/**
* Get and check the required signature.
*/
@@ -132,6 +132,7 @@ class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction,
it
}
}
+ // DOCEND 1
}
/**
diff --git a/core/src/main/kotlin/net/corda/flows/FinalityFlow.kt b/core/src/main/kotlin/net/corda/flows/FinalityFlow.kt
index ff62d89deb..e609c4f5ca 100644
--- a/core/src/main/kotlin/net/corda/flows/FinalityFlow.kt
+++ b/core/src/main/kotlin/net/corda/flows/FinalityFlow.kt
@@ -89,7 +89,7 @@ class FinalityFlow(val transactions: Iterable,
private fun needsNotarySignature(stx: SignedTransaction): Boolean {
val wtx = stx.tx
- val needsNotarisation = wtx.inputs.isNotEmpty() || wtx.timestamp != null
+ val needsNotarisation = wtx.inputs.isNotEmpty() || wtx.timeWindow != null
return needsNotarisation && hasNoNotarySignature(stx)
}
diff --git a/core/src/main/kotlin/net/corda/flows/NotaryChangeFlow.kt b/core/src/main/kotlin/net/corda/flows/NotaryChangeFlow.kt
index 8de5b096e7..f99bcd2f3a 100644
--- a/core/src/main/kotlin/net/corda/flows/NotaryChangeFlow.kt
+++ b/core/src/main/kotlin/net/corda/flows/NotaryChangeFlow.kt
@@ -7,7 +7,6 @@ import net.corda.core.identity.Party
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.ProgressTracker
-import java.security.PublicKey
/**
* A flow to be used for changing a state's Notary. This is required since all input states to a transaction
diff --git a/core/src/main/kotlin/net/corda/flows/NotaryFlow.kt b/core/src/main/kotlin/net/corda/flows/NotaryFlow.kt
index 32ff54484b..a12770ca1f 100644
--- a/core/src/main/kotlin/net/corda/flows/NotaryFlow.kt
+++ b/core/src/main/kotlin/net/corda/flows/NotaryFlow.kt
@@ -2,7 +2,7 @@ package net.corda.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.StateRef
-import net.corda.core.contracts.Timestamp
+import net.corda.core.contracts.TimeWindow
import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.SignedData
@@ -11,7 +11,7 @@ import net.corda.core.flows.FlowException
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatingFlow
import net.corda.core.identity.Party
-import net.corda.core.node.services.TimestampChecker
+import net.corda.core.node.services.TimeWindowChecker
import net.corda.core.node.services.UniquenessException
import net.corda.core.node.services.UniquenessProvider
import net.corda.core.serialization.CordaSerializable
@@ -19,17 +19,18 @@ import net.corda.core.serialization.serialize
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.unwrap
+import java.util.function.Predicate
object NotaryFlow {
/**
* A flow to be used by a party for obtaining signature(s) from a [NotaryService] ascertaining the transaction
- * timestamp is correct and none of its inputs have been used in another completed transaction.
+ * time-window is correct and none of its inputs have been used in another completed transaction.
*
* In case of a single-node or Raft notary, the flow will return a single signature. For the BFT notary multiple
* signatures will be returned – one from each replica that accepted the input state commit.
*
* @throws NotaryException in case the any of the inputs to the transaction have been consumed
- * by another transaction or the timestamp is invalid.
+ * by another transaction or the time-window is invalid.
*/
@InitiatingFlow
open class Client(private val stx: SignedTransaction,
@@ -63,7 +64,7 @@ object NotaryFlow {
val payload: Any = if (serviceHub.networkMapCache.isValidatingNotary(notaryParty)) {
stx
} else {
- wtx.buildFilteredTransaction { it is StateRef || it is Timestamp }
+ wtx.buildFilteredTransaction(Predicate { it is StateRef || it is TimeWindow })
}
val response = try {
@@ -90,19 +91,19 @@ object NotaryFlow {
/**
* A flow run by a notary service that handles notarisation requests.
*
- * It checks that the timestamp command is valid (if present) and commits the input state, or returns a conflict
+ * It checks that the time-window command is valid (if present) and commits the input state, or returns a conflict
* if any of the input states have been previously committed.
*
* Additional transaction validation logic can be added when implementing [receiveAndVerifyTx].
*/
// See AbstractStateReplacementFlow.Acceptor for why it's Void?
abstract class Service(val otherSide: Party,
- val timestampChecker: TimestampChecker,
+ val timeWindowChecker: TimeWindowChecker,
val uniquenessProvider: UniquenessProvider) : FlowLogic() {
@Suspendable
override fun call(): Void? {
- val (id, inputs, timestamp) = receiveAndVerifyTx()
- validateTimestamp(timestamp)
+ val (id, inputs, timeWindow) = receiveAndVerifyTx()
+ validateTimeWindow(timeWindow)
commitInputStates(inputs, id)
signAndSendResponse(id)
return null
@@ -121,9 +122,9 @@ object NotaryFlow {
send(otherSide, listOf(signature))
}
- private fun validateTimestamp(t: Timestamp?) {
- if (t != null && !timestampChecker.isValid(t))
- throw NotaryException(NotaryError.TimestampInvalid)
+ private fun validateTimeWindow(t: TimeWindow?) {
+ if (t != null && !timeWindowChecker.isValid(t))
+ throw NotaryException(NotaryError.TimeWindowInvalid)
}
/**
@@ -162,7 +163,7 @@ object NotaryFlow {
* The minimum amount of information needed to notarise a transaction. Note that this does not include
* any sensitive transaction details.
*/
-data class TransactionParts(val id: SecureHash, val inputs: List, val timestamp: Timestamp?)
+data class TransactionParts(val id: SecureHash, val inputs: List, val timestamp: TimeWindow?)
class NotaryException(val error: NotaryError) : FlowException("Error response from Notary - $error")
@@ -172,8 +173,8 @@ sealed class NotaryError {
override fun toString() = "One or more input states for transaction $txId have been used in another transaction"
}
- /** Thrown if the time specified in the timestamp command is outside the allowed tolerance */
- object TimestampInvalid : NotaryError()
+ /** Thrown if the time specified in the [TimeWindow] command is outside the allowed tolerance. */
+ object TimeWindowInvalid : NotaryError()
data class TransactionInvalid(val msg: String) : NotaryError()
data class SignaturesInvalid(val msg: String) : NotaryError()
diff --git a/core/src/main/kotlin/net/corda/flows/TxKeyFlow.kt b/core/src/main/kotlin/net/corda/flows/TxKeyFlow.kt
new file mode 100644
index 0000000000..ff50739ffc
--- /dev/null
+++ b/core/src/main/kotlin/net/corda/flows/TxKeyFlow.kt
@@ -0,0 +1,89 @@
+package net.corda.flows
+
+import co.paralleluniverse.fibers.Suspendable
+import net.corda.core.flows.FlowLogic
+import net.corda.core.flows.InitiatedBy
+import net.corda.core.flows.InitiatingFlow
+import net.corda.core.flows.StartableByRPC
+import net.corda.core.identity.AnonymousParty
+import net.corda.core.identity.Party
+import net.corda.core.serialization.CordaSerializable
+import net.corda.core.utilities.ProgressTracker
+import net.corda.core.utilities.unwrap
+import org.bouncycastle.cert.X509CertificateHolder
+import java.security.cert.CertPath
+
+/**
+ * Very basic flow which exchanges transaction key and certificate paths between two parties in a transaction.
+ * This is intended for use as a subflow of another flow.
+ */
+object TxKeyFlow {
+ abstract class AbstractIdentityFlow(val otherSide: Party, val revocationEnabled: Boolean): FlowLogic() {
+ fun validateIdentity(untrustedIdentity: AnonymousIdentity): AnonymousIdentity {
+ val (certPath, theirCert, txIdentity) = untrustedIdentity
+ if (theirCert.subject == otherSide.name) {
+ serviceHub.identityService.registerAnonymousIdentity(txIdentity, otherSide, certPath)
+ return AnonymousIdentity(certPath, theirCert, txIdentity)
+ } else
+ throw IllegalStateException("Expected certificate subject to be ${otherSide.name} but found ${theirCert.subject}")
+ }
+ }
+
+ @StartableByRPC
+ @InitiatingFlow
+ class Requester(otherSide: Party,
+ override val progressTracker: ProgressTracker) : AbstractIdentityFlow