From a33036083470ca2df0557178639c49f1af39e842 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Thu, 22 Jun 2017 14:31:52 +0100 Subject: [PATCH 01/97] Add Interface synthesis to the carpenter Currently the carpenter only generates concreate classes, add the ability to also synthesise interfaces and have those interfaces used by other gnerated classes --- .../net/corda/carpenter/ClassCarpenter.kt | 58 +++++++++- .../net/corda/carpenter/ClassCarpenterTest.kt | 108 +++++++++++++++--- 2 files changed, 149 insertions(+), 17 deletions(-) diff --git a/experimental/src/main/kotlin/net/corda/carpenter/ClassCarpenter.kt b/experimental/src/main/kotlin/net/corda/carpenter/ClassCarpenter.kt index 7641b9c6ea..bc29f684ed 100644 --- a/experimental/src/main/kotlin/net/corda/carpenter/ClassCarpenter.kt +++ b/experimental/src/main/kotlin/net/corda/carpenter/ClassCarpenter.kt @@ -71,7 +71,7 @@ class ClassCarpenter { /** * A Schema represents a desired class. */ - class Schema(val name: String, fields: Map>, val superclass: Schema? = null, val interfaces: List> = emptyList()) { + open class Schema(val name: String, fields: Map>, val superclass: Schema? = null, val interfaces: List> = emptyList()) { val fields = LinkedHashMap(fields) // Fix the order up front if the user didn't. val descriptors = fields.map { it.key to Type.getDescriptor(it.value) }.toMap() @@ -79,6 +79,20 @@ class ClassCarpenter { fun descriptorsIncludingSuperclasses(): Map = (superclass?.descriptorsIncludingSuperclasses() ?: emptyMap()) + LinkedHashMap(descriptors) } + class ClassSchema( + name: String, + fields: Map>, + superclass: Schema? = null, + interfaces: List> = emptyList() + ) : Schema (name, fields, superclass, interfaces) + + class InterfaceSchema( + name: String, + fields: Map>, + superclass: Schema? = null, + interfaces: List> = emptyList() + ) : Schema (name, fields, superclass, interfaces) + class DuplicateName : RuntimeException("An attempt was made to register two classes with the same name within the same ClassCarpenter namespace.") class InterfaceMismatch(msg: String) : RuntimeException(msg) @@ -111,11 +125,16 @@ class ClassCarpenter { hierarchy += cursor cursor = cursor.superclass } - hierarchy.reversed().forEach { generateClass(it) } + hierarchy.reversed().forEach { + when (it) { + is ClassSchema -> generateClass(it) + is InterfaceSchema -> generateInterface(it) + } + } return _loaded[schema.name]!! } - private fun generateClass(schema: Schema): Class<*> { + private fun generateClass(schema: ClassSchema): Class<*> { val jvmName = schema.name.jvm // Lazy: we could compute max locals/max stack ourselves, it'd be faster. val cw = ClassWriter(ClassWriter.COMPUTE_FRAMES or ClassWriter.COMPUTE_MAXS) @@ -137,6 +156,25 @@ class ClassCarpenter { return clazz } + private fun generateInterface(schema: InterfaceSchema): Class<*> { + val jvmName = schema.name.jvm + // Lazy: we could compute max locals/max stack ourselves, it'd be faster. + val cw = ClassWriter (ClassWriter.COMPUTE_FRAMES or ClassWriter.COMPUTE_MAXS) + with(cw) { + val interfaces = schema.interfaces.map { it.name.jvm }.toTypedArray() + + visit(V1_8, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE, jvmName, null, "java/lang/Object", interfaces) + + generateAbstractGetters(schema) + + visitEnd() + } + val clazz = classloader.load(schema.name, cw.toByteArray()) + + _loaded[schema.name] = clazz + return clazz + } + private fun ClassWriter.generateFields(schema: Schema) { for ((name, desc) in schema.descriptors) { visitField(ACC_PROTECTED + ACC_FINAL, name, desc, null, null).visitEnd() @@ -202,6 +240,17 @@ class ClassCarpenter { } } + private fun ClassWriter.generateAbstractGetters(schema: InterfaceSchema) { + for ((name, type) in schema.fields) { + val descriptor = schema.descriptors[name] + val opcodes = ACC_ABSTRACT + ACC_PUBLIC + with (visitMethod(opcodes, "get" + name.capitalize(), "()" + descriptor, null, null)) { + // abstract method doesn't have any implementation so just end + visitEnd() + } + } + } + private fun ClassWriter.generateConstructor(jvmName: String, schema: Schema) { with(visitMethod(ACC_PUBLIC, "", "(" + schema.descriptorsIncludingSuperclasses().values.joinToString("") + ")V", null, null)) { visitCode() @@ -262,7 +311,8 @@ class ClassCarpenter { method.name.startsWith("get") -> method.name.substring(3).decapitalize() else -> throw InterfaceMismatch("Requested interfaces must consist only of methods that start with 'get': ${itf.name}.${method.name}") } - if (fieldNameFromItf !in allFields) + + if ((schema is ClassSchema) and (fieldNameFromItf !in allFields)) throw InterfaceMismatch("Interface ${itf.name} requires a field named ${fieldNameFromItf} but that isn't found in the schema or any superclass schemas") } } diff --git a/experimental/src/test/kotlin/net/corda/carpenter/ClassCarpenterTest.kt b/experimental/src/test/kotlin/net/corda/carpenter/ClassCarpenterTest.kt index 5ffe32e51d..2e6b6bebd8 100644 --- a/experimental/src/test/kotlin/net/corda/carpenter/ClassCarpenterTest.kt +++ b/experimental/src/test/kotlin/net/corda/carpenter/ClassCarpenterTest.kt @@ -21,7 +21,7 @@ class ClassCarpenterTest { @Test fun empty() { - val clazz = cc.build(ClassCarpenter.Schema("gen.EmptyClass", emptyMap(), null)) + val clazz = cc.build(ClassCarpenter.ClassSchema("gen.EmptyClass", emptyMap(), null)) assertEquals(0, clazz.nonSyntheticFields.size) assertEquals(2, clazz.nonSyntheticMethods.size) // get, toString assertEquals(0, clazz.declaredConstructors[0].parameterCount) @@ -30,7 +30,7 @@ class ClassCarpenterTest { @Test fun prims() { - val clazz = cc.build(ClassCarpenter.Schema("gen.Prims", mapOf( + val clazz = cc.build(ClassCarpenter.ClassSchema("gen.Prims", mapOf( "anIntField" to Int::class.javaPrimitiveType!!, "aLongField" to Long::class.javaPrimitiveType!!, "someCharField" to Char::class.javaPrimitiveType!!, @@ -65,7 +65,7 @@ class ClassCarpenterTest { } private fun genPerson(): Pair, Any> { - val clazz = cc.build(ClassCarpenter.Schema("gen.Person", mapOf( + val clazz = cc.build(ClassCarpenter.ClassSchema("gen.Person", mapOf( "age" to Int::class.javaPrimitiveType!!, "name" to String::class.java ))) @@ -88,14 +88,14 @@ class ClassCarpenterTest { @Test(expected = ClassCarpenter.DuplicateName::class) fun duplicates() { - cc.build(ClassCarpenter.Schema("gen.EmptyClass", emptyMap(), null)) - cc.build(ClassCarpenter.Schema("gen.EmptyClass", emptyMap(), null)) + cc.build(ClassCarpenter.ClassSchema("gen.EmptyClass", emptyMap(), null)) + cc.build(ClassCarpenter.ClassSchema("gen.EmptyClass", emptyMap(), null)) } @Test fun `can refer to each other`() { val (clazz1, i) = genPerson() - val clazz2 = cc.build(ClassCarpenter.Schema("gen.Referee", mapOf( + val clazz2 = cc.build(ClassCarpenter.ClassSchema("gen.Referee", mapOf( "ref" to clazz1 ))) val i2 = clazz2.constructors[0].newInstance(i) @@ -104,8 +104,8 @@ class ClassCarpenterTest { @Test fun superclasses() { - val schema1 = ClassCarpenter.Schema("gen.A", mapOf("a" to String::class.java)) - val schema2 = ClassCarpenter.Schema("gen.B", mapOf("b" to String::class.java), schema1) + val schema1 = ClassCarpenter.ClassSchema("gen.A", mapOf("a" to String::class.java)) + val schema2 = ClassCarpenter.ClassSchema("gen.B", mapOf("b" to String::class.java), schema1) val clazz = cc.build(schema2) val i = clazz.constructors[0].newInstance("xa", "xb") as SimpleFieldAccess assertEquals("xa", i["a"]) @@ -115,8 +115,8 @@ class ClassCarpenterTest { @Test fun interfaces() { - val schema1 = ClassCarpenter.Schema("gen.A", mapOf("a" to String::class.java)) - val schema2 = ClassCarpenter.Schema("gen.B", mapOf("b" to Int::class.java), schema1, interfaces = listOf(DummyInterface::class.java)) + val schema1 = ClassCarpenter.ClassSchema("gen.A", mapOf("a" to String::class.java)) + val schema2 = ClassCarpenter.ClassSchema("gen.B", mapOf("b" to Int::class.java), schema1, interfaces = listOf(DummyInterface::class.java)) val clazz = cc.build(schema2) val i = clazz.constructors[0].newInstance("xa", 1) as DummyInterface assertEquals("xa", i.a) @@ -125,10 +125,92 @@ class ClassCarpenterTest { @Test(expected = ClassCarpenter.InterfaceMismatch::class) fun `mismatched interface`() { - val schema1 = ClassCarpenter.Schema("gen.A", mapOf("a" to String::class.java)) - val schema2 = ClassCarpenter.Schema("gen.B", mapOf("c" to Int::class.java), schema1, interfaces = listOf(DummyInterface::class.java)) + val schema1 = ClassCarpenter.ClassSchema("gen.A", mapOf("a" to String::class.java)) + val schema2 = ClassCarpenter.ClassSchema("gen.B", mapOf("c" to Int::class.java), schema1, interfaces = listOf(DummyInterface::class.java)) val clazz = cc.build(schema2) val i = clazz.constructors[0].newInstance("xa", 1) as DummyInterface assertEquals(1, i.b) } -} \ No newline at end of file + + @Test + fun `generate interface`() { + val schema1 = ClassCarpenter.InterfaceSchema("gen.Interface", mapOf("a" to Int::class.java)) + val iface = cc.build(schema1) + + assert (iface.isInterface()) + assert (iface.constructors.isEmpty()) + assertEquals (iface.declaredMethods.size, 1) + assertEquals (iface.declaredMethods[0].name, "getA") + + val schema2 = ClassCarpenter.ClassSchema("gen.Derived", mapOf("a" to Int::class.java), interfaces = listOf (iface)) + val clazz = cc.build(schema2) + val testA = 42 + val i = clazz.constructors[0].newInstance(testA) as SimpleFieldAccess + + assertEquals(testA, i["a"]) + } + + @Test + fun `generate multiple interfaces`() { + val iFace1 = ClassCarpenter.InterfaceSchema("gen.Interface1", mapOf("a" to Int::class.java, "b" to String::class.java)) + val iFace2 = ClassCarpenter.InterfaceSchema("gen.Interface2", mapOf("c" to Int::class.java, "d" to String::class.java)) + + val class1 = ClassCarpenter.ClassSchema( + "gen.Derived", + mapOf( + "a" to Int::class.java, + "b" to String::class.java, + "c" to Int::class.java, + "d" to String::class.java), + interfaces = listOf (cc.build (iFace1), cc.build (iFace2))) + + val clazz = cc.build(class1) + val testA = 42 + val testB = "don't touch me, I'm scared" + val testC = 0xDEAD + val testD = "wibble" + val i = clazz.constructors[0].newInstance(testA, testB, testC, testD) as SimpleFieldAccess + + assertEquals(testA, i["a"]) + assertEquals(testB, i["b"]) + assertEquals(testC, i["c"]) + assertEquals(testD, i["d"]) + } + + @Test + fun `interface implementing interface`() { + val iFace1 = ClassCarpenter.InterfaceSchema( + "gen.Interface1", + mapOf ( + "a" to Int::class.java, + "b" to String::class.java)) + + val iFace2 = ClassCarpenter.InterfaceSchema( + "gen.Interface2", + mapOf( + "c" to Int::class.java, + "d" to String::class.java), + interfaces = listOf (cc.build (iFace1))) + + val class1 = ClassCarpenter.ClassSchema( + "gen.Derived", + mapOf( + "a" to Int::class.java, + "b" to String::class.java, + "c" to Int::class.java, + "d" to String::class.java), + interfaces = listOf (cc.build (iFace2))) + + val clazz = cc.build(class1) + val testA = 99 + val testB = "green is not a creative colour" + val testC = 7 + val testD = "I like jam" + val i = clazz.constructors[0].newInstance(testA, testB, testC, testD) as SimpleFieldAccess + + assertEquals(testA, i["a"]) + assertEquals(testB, i["b"]) + assertEquals(testC, i["c"]) + assertEquals(testD, i["d"]) + } +} From 9d2905e12544d66026f163f9c90198495abae411 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Thu, 22 Jun 2017 16:02:02 +0100 Subject: [PATCH 02/97] Review Comments Refactor generator calls to use a visitor type pattern to avoid duplicating the class writer boiler plate. Each generate call passes its own function to the wrapping class that writes the boiler plate and then calls back into the type specific methods before returning bacl to finlaise the class writer and load it into the class loader Remove unkotlinesq spaces --- .../net/corda/carpenter/ClassCarpenter.kt | 92 ++++++++++--------- .../net/corda/carpenter/ClassCarpenterTest.kt | 18 ++-- 2 files changed, 60 insertions(+), 50 deletions(-) diff --git a/experimental/src/main/kotlin/net/corda/carpenter/ClassCarpenter.kt b/experimental/src/main/kotlin/net/corda/carpenter/ClassCarpenter.kt index bc29f684ed..112426729b 100644 --- a/experimental/src/main/kotlin/net/corda/carpenter/ClassCarpenter.kt +++ b/experimental/src/main/kotlin/net/corda/carpenter/ClassCarpenter.kt @@ -77,8 +77,13 @@ class ClassCarpenter { fun fieldsIncludingSuperclasses(): Map> = (superclass?.fieldsIncludingSuperclasses() ?: emptyMap()) + LinkedHashMap(fields) fun descriptorsIncludingSuperclasses(): Map = (superclass?.descriptorsIncludingSuperclasses() ?: emptyMap()) + LinkedHashMap(descriptors) + + val jvmName : String + get() = name.replace (".", "/") } + private val String.jvm: String get() = replace(".", "/") + class ClassSchema( name: String, fields: Map>, @@ -106,8 +111,6 @@ class ClassCarpenter { /** Returns a snapshot of the currently loaded classes as a map of full class name (package names+dots) -> class object */ val loaded: Map> = HashMap(_loaded) - private val String.jvm: String get() = replace(".", "/") - /** * Generate bytecode for the given schema and load into the JVM. The returned class object can be used to * construct instances of the generated class. @@ -125,52 +128,59 @@ class ClassCarpenter { hierarchy += cursor cursor = cursor.superclass } + hierarchy.reversed().forEach { when (it) { - is ClassSchema -> generateClass(it) is InterfaceSchema -> generateInterface(it) + is ClassSchema -> generateClass(it) } } + return _loaded[schema.name]!! } - private fun generateClass(schema: ClassSchema): Class<*> { - val jvmName = schema.name.jvm - // Lazy: we could compute max locals/max stack ourselves, it'd be faster. - val cw = ClassWriter(ClassWriter.COMPUTE_FRAMES or ClassWriter.COMPUTE_MAXS) - with(cw) { - // public class Name implements SimpleFieldAccess { - val superName = schema.superclass?.name?.jvm ?: "java/lang/Object" - val interfaces = arrayOf(SimpleFieldAccess::class.java.name.jvm) + schema.interfaces.map { it.name.jvm } - visit(52, ACC_PUBLIC + ACC_SUPER, jvmName, null, superName, interfaces) - generateFields(schema) - generateConstructor(jvmName, schema) - generateGetters(jvmName, schema) - if (schema.superclass == null) - generateGetMethod() // From SimplePropertyAccess - generateToString(jvmName, schema) - visitEnd() - } - val clazz = classloader.load(schema.name, cw.toByteArray()) - _loaded[schema.name] = clazz - return clazz - } - - private fun generateInterface(schema: InterfaceSchema): Class<*> { - val jvmName = schema.name.jvm - // Lazy: we could compute max locals/max stack ourselves, it'd be faster. - val cw = ClassWriter (ClassWriter.COMPUTE_FRAMES or ClassWriter.COMPUTE_MAXS) - with(cw) { + private fun generateInterface (schema: Schema): Class<*> { + return generate (schema) { cw, schema -> val interfaces = schema.interfaces.map { it.name.jvm }.toTypedArray() - visit(V1_8, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE, jvmName, null, "java/lang/Object", interfaces) + with (cw) { + visit(V1_8, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE, schema.jvmName, null, "java/lang/Object", interfaces) - generateAbstractGetters(schema) + generateAbstractGetters(schema) - visitEnd() + visitEnd() + } } - val clazz = classloader.load(schema.name, cw.toByteArray()) + } + private fun generateClass (schema: Schema): Class<*> { + return generate (schema) { cw, schema -> + val superName = schema.superclass?.jvmName ?: "java/lang/Object" + val interfaces = arrayOf(SimpleFieldAccess::class.java.name.jvm) + schema.interfaces.map { it.name.jvm } + + with (cw) { + visit(V1_8, ACC_PUBLIC + ACC_SUPER, schema.jvmName, null, superName, interfaces) + + generateFields(schema) + generateConstructor(schema) + generateGetters(schema) + if (schema.superclass == null) + generateGetMethod() // From SimplePropertyAccess + generateToString(schema) + + visitEnd() + } + } + + } + + private fun generate(schema: Schema, generator : (ClassWriter, Schema) -> Unit): Class<*> { + // Lazy: we could compute max locals/max stack ourselves, it'd be faster. + val cw = ClassWriter (ClassWriter.COMPUTE_FRAMES or ClassWriter.COMPUTE_MAXS) + + generator (cw, schema) + + val clazz = classloader.load(schema.name, cw.toByteArray()) _loaded[schema.name] = clazz return clazz } @@ -181,7 +191,7 @@ class ClassCarpenter { } } - private fun ClassWriter.generateToString(jvmName: String, schema: Schema) { + private fun ClassWriter.generateToString(schema: Schema) { val toStringHelper = "com/google/common/base/MoreObjects\$ToStringHelper" with(visitMethod(ACC_PUBLIC, "toString", "()Ljava/lang/String;", "", null)) { visitCode() @@ -192,7 +202,7 @@ class ClassCarpenter { for ((name, type) in schema.fieldsIncludingSuperclasses().entries) { visitLdcInsn(name) visitVarInsn(ALOAD, 0) // this - visitFieldInsn(GETFIELD, jvmName, name, schema.descriptorsIncludingSuperclasses()[name]) + visitFieldInsn(GETFIELD, schema.jvmName, name, schema.descriptorsIncludingSuperclasses()[name]) val desc = if (type.isPrimitive) schema.descriptors[name] else "Ljava/lang/Object;" visitMethodInsn(INVOKEVIRTUAL, toStringHelper, "add", "(Ljava/lang/String;$desc)L$toStringHelper;", false) } @@ -220,13 +230,13 @@ class ClassCarpenter { } } - private fun ClassWriter.generateGetters(jvmName: String, schema: Schema) { + private fun ClassWriter.generateGetters(schema: Schema) { for ((name, type) in schema.fields) { val descriptor = schema.descriptors[name] with(visitMethod(ACC_PUBLIC, "get" + name.capitalize(), "()" + descriptor, null, null)) { visitCode() visitVarInsn(ALOAD, 0) // Load 'this' - visitFieldInsn(GETFIELD, jvmName, name, descriptor) + visitFieldInsn(GETFIELD, schema.jvmName, name, descriptor) when (type) { java.lang.Boolean.TYPE, Integer.TYPE, java.lang.Short.TYPE, java.lang.Byte.TYPE, TYPE -> visitInsn(IRETURN) java.lang.Long.TYPE -> visitInsn(LRETURN) @@ -240,7 +250,7 @@ class ClassCarpenter { } } - private fun ClassWriter.generateAbstractGetters(schema: InterfaceSchema) { + private fun ClassWriter.generateAbstractGetters(schema: Schema) { for ((name, type) in schema.fields) { val descriptor = schema.descriptors[name] val opcodes = ACC_ABSTRACT + ACC_PUBLIC @@ -251,7 +261,7 @@ class ClassCarpenter { } } - private fun ClassWriter.generateConstructor(jvmName: String, schema: Schema) { + private fun ClassWriter.generateConstructor(schema: Schema) { with(visitMethod(ACC_PUBLIC, "", "(" + schema.descriptorsIncludingSuperclasses().values.joinToString("") + ")V", null, null)) { visitCode() // Calculate the super call. @@ -273,7 +283,7 @@ class ClassCarpenter { throw UnsupportedOperationException("Array types are not implemented yet") visitVarInsn(ALOAD, 0) // Load 'this' onto the stack slot += load(slot, type) // Load the contents of the parameter onto the stack. - visitFieldInsn(PUTFIELD, jvmName, name, schema.descriptors[name]) + visitFieldInsn(PUTFIELD, schema.jvmName, name, schema.descriptors[name]) } visitInsn(RETURN) visitMaxs(0, 0) diff --git a/experimental/src/test/kotlin/net/corda/carpenter/ClassCarpenterTest.kt b/experimental/src/test/kotlin/net/corda/carpenter/ClassCarpenterTest.kt index 2e6b6bebd8..27fe55ea2d 100644 --- a/experimental/src/test/kotlin/net/corda/carpenter/ClassCarpenterTest.kt +++ b/experimental/src/test/kotlin/net/corda/carpenter/ClassCarpenterTest.kt @@ -137,12 +137,12 @@ class ClassCarpenterTest { val schema1 = ClassCarpenter.InterfaceSchema("gen.Interface", mapOf("a" to Int::class.java)) val iface = cc.build(schema1) - assert (iface.isInterface()) - assert (iface.constructors.isEmpty()) - assertEquals (iface.declaredMethods.size, 1) - assertEquals (iface.declaredMethods[0].name, "getA") + assert(iface.isInterface()) + assert(iface.constructors.isEmpty()) + assertEquals(iface.declaredMethods.size, 1) + assertEquals(iface.declaredMethods[0].name, "getA") - val schema2 = ClassCarpenter.ClassSchema("gen.Derived", mapOf("a" to Int::class.java), interfaces = listOf (iface)) + val schema2 = ClassCarpenter.ClassSchema("gen.Derived", mapOf("a" to Int::class.java), interfaces = listOf(iface)) val clazz = cc.build(schema2) val testA = 42 val i = clazz.constructors[0].newInstance(testA) as SimpleFieldAccess @@ -162,7 +162,7 @@ class ClassCarpenterTest { "b" to String::class.java, "c" to Int::class.java, "d" to String::class.java), - interfaces = listOf (cc.build (iFace1), cc.build (iFace2))) + interfaces = listOf(cc.build(iFace1), cc.build(iFace2))) val clazz = cc.build(class1) val testA = 42 @@ -181,7 +181,7 @@ class ClassCarpenterTest { fun `interface implementing interface`() { val iFace1 = ClassCarpenter.InterfaceSchema( "gen.Interface1", - mapOf ( + mapOf( "a" to Int::class.java, "b" to String::class.java)) @@ -190,7 +190,7 @@ class ClassCarpenterTest { mapOf( "c" to Int::class.java, "d" to String::class.java), - interfaces = listOf (cc.build (iFace1))) + interfaces = listOf(cc.build(iFace1))) val class1 = ClassCarpenter.ClassSchema( "gen.Derived", @@ -199,7 +199,7 @@ class ClassCarpenterTest { "b" to String::class.java, "c" to Int::class.java, "d" to String::class.java), - interfaces = listOf (cc.build (iFace2))) + interfaces = listOf(cc.build(iFace2))) val clazz = cc.build(class1) val testA = 99 From de9a3da572ba7eff149a12213a55c86625ebf8f6 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Thu, 22 Jun 2017 16:28:02 +0100 Subject: [PATCH 03/97] Remove warnings --- .../main/kotlin/net/corda/carpenter/ClassCarpenter.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/experimental/src/main/kotlin/net/corda/carpenter/ClassCarpenter.kt b/experimental/src/main/kotlin/net/corda/carpenter/ClassCarpenter.kt index 112426729b..bf45d7cf82 100644 --- a/experimental/src/main/kotlin/net/corda/carpenter/ClassCarpenter.kt +++ b/experimental/src/main/kotlin/net/corda/carpenter/ClassCarpenter.kt @@ -251,7 +251,7 @@ class ClassCarpenter { } private fun ClassWriter.generateAbstractGetters(schema: Schema) { - for ((name, type) in schema.fields) { + for ((name, _) in schema.fields) { val descriptor = schema.descriptors[name] val opcodes = ACC_ABSTRACT + ACC_PUBLIC with (visitMethod(opcodes, "get" + name.capitalize(), "()" + descriptor, null, null)) { @@ -316,14 +316,14 @@ class ClassCarpenter { // actually called, which is a bit too dynamic for my tastes. val allFields = schema.fieldsIncludingSuperclasses() for (itf in schema.interfaces) { - for (method in itf.methods) { + itf.methods.forEach { val fieldNameFromItf = when { - method.name.startsWith("get") -> method.name.substring(3).decapitalize() - else -> throw InterfaceMismatch("Requested interfaces must consist only of methods that start with 'get': ${itf.name}.${method.name}") + it.name.startsWith("get") -> it.name.substring(3).decapitalize() + else -> throw InterfaceMismatch("Requested interfaces must consist only of methods that start with 'get': ${itf.name}.${it.name}") } if ((schema is ClassSchema) and (fieldNameFromItf !in allFields)) - throw InterfaceMismatch("Interface ${itf.name} requires a field named ${fieldNameFromItf} but that isn't found in the schema or any superclass schemas") + throw InterfaceMismatch("Interface ${itf.name} requires a field named $fieldNameFromItf but that isn't found in the schema or any superclass schemas") } } } From b356220da1c34ad566da0ccb4337b9609a9bcef7 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Thu, 22 Jun 2017 16:34:58 +0100 Subject: [PATCH 04/97] Remove blank line --- .../src/main/kotlin/net/corda/carpenter/ClassCarpenter.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/experimental/src/main/kotlin/net/corda/carpenter/ClassCarpenter.kt b/experimental/src/main/kotlin/net/corda/carpenter/ClassCarpenter.kt index bf45d7cf82..983b31c248 100644 --- a/experimental/src/main/kotlin/net/corda/carpenter/ClassCarpenter.kt +++ b/experimental/src/main/kotlin/net/corda/carpenter/ClassCarpenter.kt @@ -171,7 +171,6 @@ class ClassCarpenter { visitEnd() } } - } private fun generate(schema: Schema, generator : (ClassWriter, Schema) -> Unit): Class<*> { From 28b7610e47ae4225bf531f5d4b76acb1352adb2b Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Fri, 23 Jun 2017 09:02:00 +0100 Subject: [PATCH 05/97] IntelliJ reformat of the code --- .../net/corda/carpenter/ClassCarpenter.kt | 53 +++++++------- .../net/corda/carpenter/ClassCarpenterTest.kt | 72 +++++++++---------- 2 files changed, 63 insertions(+), 62 deletions(-) diff --git a/experimental/src/main/kotlin/net/corda/carpenter/ClassCarpenter.kt b/experimental/src/main/kotlin/net/corda/carpenter/ClassCarpenter.kt index 983b31c248..534083325f 100644 --- a/experimental/src/main/kotlin/net/corda/carpenter/ClassCarpenter.kt +++ b/experimental/src/main/kotlin/net/corda/carpenter/ClassCarpenter.kt @@ -76,27 +76,27 @@ class ClassCarpenter { val descriptors = fields.map { it.key to Type.getDescriptor(it.value) }.toMap() fun fieldsIncludingSuperclasses(): Map> = (superclass?.fieldsIncludingSuperclasses() ?: emptyMap()) + LinkedHashMap(fields) - fun descriptorsIncludingSuperclasses(): Map = (superclass?.descriptorsIncludingSuperclasses() ?: emptyMap()) + LinkedHashMap(descriptors) + fun descriptorsIncludingSuperclasses(): Map = (superclass?.descriptorsIncludingSuperclasses() ?: emptyMap()) + LinkedHashMap(descriptors) - val jvmName : String - get() = name.replace (".", "/") + val jvmName: String + get() = name.replace(".", "/") } private val String.jvm: String get() = replace(".", "/") class ClassSchema( - name: String, - fields: Map>, - superclass: Schema? = null, - interfaces: List> = emptyList() - ) : Schema (name, fields, superclass, interfaces) + name: String, + fields: Map>, + superclass: Schema? = null, + interfaces: List> = emptyList() + ) : Schema(name, fields, superclass, interfaces) class InterfaceSchema( - name: String, - fields: Map>, - superclass: Schema? = null, - interfaces: List> = emptyList() - ) : Schema (name, fields, superclass, interfaces) + name: String, + fields: Map>, + superclass: Schema? = null, + interfaces: List> = emptyList() + ) : Schema(name, fields, superclass, interfaces) class DuplicateName : RuntimeException("An attempt was made to register two classes with the same name within the same ClassCarpenter namespace.") class InterfaceMismatch(msg: String) : RuntimeException(msg) @@ -104,6 +104,7 @@ class ClassCarpenter { private class CarpenterClassLoader : ClassLoader(Thread.currentThread().contextClassLoader) { fun load(name: String, bytes: ByteArray) = defineClass(name, bytes, 0, bytes.size) } + private val classloader = CarpenterClassLoader() private val _loaded = HashMap>() @@ -132,18 +133,18 @@ class ClassCarpenter { hierarchy.reversed().forEach { when (it) { is InterfaceSchema -> generateInterface(it) - is ClassSchema -> generateClass(it) + is ClassSchema -> generateClass(it) } } return _loaded[schema.name]!! } - private fun generateInterface (schema: Schema): Class<*> { - return generate (schema) { cw, schema -> + private fun generateInterface(schema: Schema): Class<*> { + return generate(schema) { cw, schema -> val interfaces = schema.interfaces.map { it.name.jvm }.toTypedArray() - with (cw) { + with(cw) { visit(V1_8, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE, schema.jvmName, null, "java/lang/Object", interfaces) generateAbstractGetters(schema) @@ -153,12 +154,12 @@ class ClassCarpenter { } } - private fun generateClass (schema: Schema): Class<*> { - return generate (schema) { cw, schema -> - val superName = schema.superclass?.jvmName ?: "java/lang/Object" + private fun generateClass(schema: Schema): Class<*> { + return generate(schema) { cw, schema -> + val superName = schema.superclass?.jvmName ?: "java/lang/Object" val interfaces = arrayOf(SimpleFieldAccess::class.java.name.jvm) + schema.interfaces.map { it.name.jvm } - with (cw) { + with(cw) { visit(V1_8, ACC_PUBLIC + ACC_SUPER, schema.jvmName, null, superName, interfaces) generateFields(schema) @@ -173,11 +174,11 @@ class ClassCarpenter { } } - private fun generate(schema: Schema, generator : (ClassWriter, Schema) -> Unit): Class<*> { + private fun generate(schema: Schema, generator: (ClassWriter, Schema) -> Unit): Class<*> { // Lazy: we could compute max locals/max stack ourselves, it'd be faster. - val cw = ClassWriter (ClassWriter.COMPUTE_FRAMES or ClassWriter.COMPUTE_MAXS) + val cw = ClassWriter(ClassWriter.COMPUTE_FRAMES or ClassWriter.COMPUTE_MAXS) - generator (cw, schema) + generator(cw, schema) val clazz = classloader.load(schema.name, cw.toByteArray()) _loaded[schema.name] = clazz @@ -252,8 +253,8 @@ class ClassCarpenter { private fun ClassWriter.generateAbstractGetters(schema: Schema) { for ((name, _) in schema.fields) { val descriptor = schema.descriptors[name] - val opcodes = ACC_ABSTRACT + ACC_PUBLIC - with (visitMethod(opcodes, "get" + name.capitalize(), "()" + descriptor, null, null)) { + val opcodes = ACC_ABSTRACT + ACC_PUBLIC + with(visitMethod(opcodes, "get" + name.capitalize(), "()" + descriptor, null, null)) { // abstract method doesn't have any implementation so just end visitEnd() } diff --git a/experimental/src/test/kotlin/net/corda/carpenter/ClassCarpenterTest.kt b/experimental/src/test/kotlin/net/corda/carpenter/ClassCarpenterTest.kt index 27fe55ea2d..1827c83d79 100644 --- a/experimental/src/test/kotlin/net/corda/carpenter/ClassCarpenterTest.kt +++ b/experimental/src/test/kotlin/net/corda/carpenter/ClassCarpenterTest.kt @@ -143,9 +143,9 @@ class ClassCarpenterTest { assertEquals(iface.declaredMethods[0].name, "getA") val schema2 = ClassCarpenter.ClassSchema("gen.Derived", mapOf("a" to Int::class.java), interfaces = listOf(iface)) - val clazz = cc.build(schema2) - val testA = 42 - val i = clazz.constructors[0].newInstance(testA) as SimpleFieldAccess + val clazz = cc.build(schema2) + val testA = 42 + val i = clazz.constructors[0].newInstance(testA) as SimpleFieldAccess assertEquals(testA, i["a"]) } @@ -156,20 +156,20 @@ class ClassCarpenterTest { val iFace2 = ClassCarpenter.InterfaceSchema("gen.Interface2", mapOf("c" to Int::class.java, "d" to String::class.java)) val class1 = ClassCarpenter.ClassSchema( - "gen.Derived", - mapOf( - "a" to Int::class.java, - "b" to String::class.java, - "c" to Int::class.java, - "d" to String::class.java), - interfaces = listOf(cc.build(iFace1), cc.build(iFace2))) + "gen.Derived", + mapOf( + "a" to Int::class.java, + "b" to String::class.java, + "c" to Int::class.java, + "d" to String::class.java), + interfaces = listOf(cc.build(iFace1), cc.build(iFace2))) val clazz = cc.build(class1) - val testA = 42 - val testB = "don't touch me, I'm scared" - val testC = 0xDEAD - val testD = "wibble" - val i = clazz.constructors[0].newInstance(testA, testB, testC, testD) as SimpleFieldAccess + val testA = 42 + val testB = "don't touch me, I'm scared" + val testC = 0xDEAD + val testD = "wibble" + val i = clazz.constructors[0].newInstance(testA, testB, testC, testD) as SimpleFieldAccess assertEquals(testA, i["a"]) assertEquals(testB, i["b"]) @@ -180,33 +180,33 @@ class ClassCarpenterTest { @Test fun `interface implementing interface`() { val iFace1 = ClassCarpenter.InterfaceSchema( - "gen.Interface1", - mapOf( - "a" to Int::class.java, - "b" to String::class.java)) + "gen.Interface1", + mapOf( + "a" to Int::class.java, + "b" to String::class.java)) val iFace2 = ClassCarpenter.InterfaceSchema( - "gen.Interface2", - mapOf( - "c" to Int::class.java, - "d" to String::class.java), - interfaces = listOf(cc.build(iFace1))) + "gen.Interface2", + mapOf( + "c" to Int::class.java, + "d" to String::class.java), + interfaces = listOf(cc.build(iFace1))) val class1 = ClassCarpenter.ClassSchema( - "gen.Derived", - mapOf( - "a" to Int::class.java, - "b" to String::class.java, - "c" to Int::class.java, - "d" to String::class.java), - interfaces = listOf(cc.build(iFace2))) + "gen.Derived", + mapOf( + "a" to Int::class.java, + "b" to String::class.java, + "c" to Int::class.java, + "d" to String::class.java), + interfaces = listOf(cc.build(iFace2))) val clazz = cc.build(class1) - val testA = 99 - val testB = "green is not a creative colour" - val testC = 7 - val testD = "I like jam" - val i = clazz.constructors[0].newInstance(testA, testB, testC, testD) as SimpleFieldAccess + val testA = 99 + val testB = "green is not a creative colour" + val testC = 7 + val testD = "I like jam" + val i = clazz.constructors[0].newInstance(testA, testB, testC, testD) as SimpleFieldAccess assertEquals(testA, i["a"]) assertEquals(testB, i["b"]) From c6d1274e47ed3a43934529f903e705ba660c9282 Mon Sep 17 00:00:00 2001 From: Andrzej Cichocki Date: Tue, 27 Jun 2017 16:34:02 +0100 Subject: [PATCH 06/97] Add tz to all non-test logging config (#894) * Use millis comma and pattern= consistently --- config/dev/log4j2.xml | 4 ++-- config/test/log4j2.xml | 2 +- samples/simm-valuation-demo/src/main/resources/log4j2.xml | 8 ++------ tools/demobench/src/main/resources/log4j2.xml | 2 +- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/config/dev/log4j2.xml b/config/dev/log4j2.xml index fa6ff74c5b..c0cb385396 100644 --- a/config/dev/log4j2.xml +++ b/config/dev/log4j2.xml @@ -13,7 +13,7 @@ - + @@ -27,7 +27,7 @@ fileName="${sys:log-path}/${log-name}.log" filePattern="${archive}/${log-name}.%date{yyyy-MM-dd}-%i.log.gz"> - + diff --git a/config/test/log4j2.xml b/config/test/log4j2.xml index adcab4aeb5..739a4d6a17 100644 --- a/config/test/log4j2.xml +++ b/config/test/log4j2.xml @@ -5,7 +5,7 @@ - + diff --git a/samples/simm-valuation-demo/src/main/resources/log4j2.xml b/samples/simm-valuation-demo/src/main/resources/log4j2.xml index 0205e4fbfe..1c4164a050 100644 --- a/samples/simm-valuation-demo/src/main/resources/log4j2.xml +++ b/samples/simm-valuation-demo/src/main/resources/log4j2.xml @@ -11,11 +11,7 @@ - - - [%-5level] %date{HH:mm:ss.SSS} [%t] %c{2}.%method - %msg%n - > - + @@ -29,7 +25,7 @@ fileName="${log-path}/${log-name}.log" filePattern="${archive}/${log-name}.%date{yyyy-MM-dd}-%i.log.gz"> - + diff --git a/tools/demobench/src/main/resources/log4j2.xml b/tools/demobench/src/main/resources/log4j2.xml index 526b1bdb46..fc1846617f 100644 --- a/tools/demobench/src/main/resources/log4j2.xml +++ b/tools/demobench/src/main/resources/log4j2.xml @@ -18,7 +18,7 @@ fileName="${sys:log-path}/${log-name}.log" filePattern="${archive}/${log-name}.%date{yyyy-MM-dd}-%i.log.gz"> - + From 4139cf497db16fbfba5cbbd105c82fe2190fed86 Mon Sep 17 00:00:00 2001 From: josecoll Date: Tue, 27 Jun 2017 17:22:06 +0100 Subject: [PATCH 07/97] Added vault query as tokenizable service. (#926) * Added vault query as tokenizable service. * Add JUnit test to test access of vault query service within a flow. * Improved JUnit test to correctly validate tokenizable behaviour. * Minor cleanup --- .../net/corda/node/internal/AbstractNode.kt | 2 +- .../statemachine/FlowFrameworkTests.kt | 36 ++++++++++++++++--- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 18b02e4e53..a0849bd84a 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -475,7 +475,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, keyManagement = makeKeyManagementService(identity) scheduler = NodeSchedulerService(services, database, unfinishedSchedules = busyNodeLatch) - val tokenizableServices = mutableListOf(storage, network, vault, keyManagement, identity, platformClock, scheduler) + val tokenizableServices = mutableListOf(storage, network, vault, vaultQuery, keyManagement, identity, platformClock, scheduler) makeAdvertisedServices(tokenizableServices) return tokenizableServices } diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt index 54412d9e67..9e4ea74112 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt @@ -5,18 +5,18 @@ import co.paralleluniverse.fibers.Suspendable import com.google.common.util.concurrent.ListenableFuture import net.corda.contracts.asset.Cash import net.corda.core.* +import net.corda.core.contracts.ContractState import net.corda.core.contracts.DOLLARS import net.corda.core.contracts.DummyState +import net.corda.core.contracts.StateAndRef import net.corda.core.crypto.SecureHash import net.corda.core.crypto.generateKeyPair -import net.corda.core.flows.FlowException -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.FlowSessionException -import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.* import net.corda.core.identity.Party import net.corda.core.messaging.MessageRecipients import net.corda.core.node.services.PartyInfo import net.corda.core.node.services.ServiceInfo +import net.corda.core.node.services.queryBy import net.corda.core.node.services.unconsumedStates import net.corda.core.serialization.OpaqueBytes import net.corda.core.serialization.deserialize @@ -597,6 +597,21 @@ class FlowFrameworkTests { assertThatThrownBy { result.getOrThrow() }.hasMessageContaining("Vault").hasMessageContaining("private method") } + @Test + fun `verify vault query service is tokenizable by force checkpointing within a flow`() { + val ptx = TransactionBuilder(notary = notary1.info.notaryIdentity) + ptx.addOutputState(DummyState()) + val stx = node1.services.signInitialTransaction(ptx) + + node1.registerFlowFactory(VaultQueryFlow::class) { + WaitingFlows.Committer(it) + } + val result = node2.services.startFlow(VaultQueryFlow(stx, node1.info.legalIdentity)).resultFuture + + mockNet.runNetwork() + assertThat(result.getOrThrow()).isEmpty() + } + @Test fun `customised client flow`() { val receiveFlowFuture = node2.registerFlowFactory(SendFlow::class) { ReceiveFlow(it) } @@ -863,6 +878,19 @@ class FlowFrameworkTests { } } + @InitiatingFlow + private class VaultQueryFlow(val stx: SignedTransaction, val otherParty: Party) : FlowLogic>>() { + @Suspendable + override fun call(): List> { + send(otherParty, stx) + // hold onto reference here to force checkpoint of vaultQueryService and thus + // prove it is registered as a tokenizableService in the node + val vaultQuerySvc = serviceHub.vaultQueryService + waitForLedgerCommit(stx.id) + return vaultQuerySvc.queryBy().states + } + } + @InitiatingFlow(version = 2) private class UpgradedFlow(val otherParty: Party) : FlowLogic() { @Suspendable From 58da76c052d2f50fca8c52f2083685e92b93a007 Mon Sep 17 00:00:00 2001 From: Katarzyna Streich Date: Tue, 27 Jun 2017 18:14:51 +0100 Subject: [PATCH 08/97] Network map redesign: Change field types in NodeInfo, move away messaging data from NodeInfo (#921) * First stage of changing fields in NodeInfo. Part of work related to NetworkMapService upgrade. Create slots for multiple IP addresses and legalIdentities per node. * NodeInfo stores HostAndPort. Move information specific to messaging layer away from NodeInfo. Only HostAndPort addresses are stored. Add peer name - peer handle mapping to MockNetwork to reflect that change. --- .../kotlin/net/corda/core/node/NodeInfo.kt | 15 +++++------ .../core/node/PhysicalLocationStructures.kt | 8 +++--- .../AttachmentSerializationTest.kt | 4 +-- .../kotlin/net/corda/flows/TxKeyFlowTests.kt | 4 +-- docs/source/changelog.rst | 6 +++++ .../corda/docs/FxTransactionBuildTutorial.kt | 2 -- .../docs/FxTransactionBuildTutorialTest.kt | 4 +-- .../WorkflowTransactionBuildTutorialTest.kt | 4 +-- .../kotlin/net/corda/flows/IssuerFlowTest.kt | 4 +-- .../nodeapi/ArtemisMessagingComponent.kt | 24 ++++++++--------- .../node/services/BFTNotaryServiceTests.kt | 2 +- .../services/messaging/P2PSecurityTest.kt | 3 ++- .../net/corda/node/internal/AbstractNode.kt | 14 +++++++--- .../kotlin/net/corda/node/internal/Node.kt | 6 +++++ .../node/services/api/ServiceHubInternal.kt | 1 + .../messaging/ArtemisMessagingServer.kt | 10 +++---- .../services/messaging/NodeMessagingClient.kt | 5 ++-- .../network/InMemoryNetworkMapCache.kt | 4 ++- .../services/network/NetworkMapService.kt | 4 +-- .../net/corda/node/CordaRPCOpsImplTest.kt | 4 +-- .../corda/node/messaging/AttachmentTests.kt | 2 +- .../node/messaging/InMemoryMessagingTests.kt | 14 +++++----- .../node/messaging/TwoPartyTradeFlowTests.kt | 22 ++++++++-------- .../corda/node/services/NotaryChangeTests.kt | 6 ++--- .../services/events/ScheduledFlowTests.kt | 4 +-- .../network/AbstractNetworkMapServiceTest.kt | 24 ++++++++--------- .../network/InMemoryNetworkMapCacheTest.kt | 2 +- .../statemachine/FlowFrameworkTests.kt | 26 +++++++++---------- .../transactions/NotaryServiceTests.kt | 2 +- .../ValidatingNotaryServiceTests.kt | 2 +- .../corda/irs/api/NodeInterestRatesTest.kt | 2 +- .../corda/irs/flows/UpdateBusinessDayFlow.kt | 2 +- .../net/corda/netmap/simulation/Simulation.kt | 12 ++++----- .../net/corda/testing/driver/DriverTests.kt | 4 +-- .../kotlin/net/corda/testing/CoreTestUtils.kt | 1 + .../testing/node/InMemoryMessagingNetwork.kt | 10 ++++--- .../corda/testing/node/MockNetworkMapCache.kt | 10 +++---- .../kotlin/net/corda/testing/node/MockNode.kt | 16 +++++++----- .../net/corda/testing/node/MockServices.kt | 6 ++++- .../net/corda/demobench/views/NodeTabView.kt | 10 +++---- .../net/corda/explorer/views/Network.kt | 4 +-- .../corda/explorer/views/TransactionViewer.kt | 1 - .../views/cordapps/cash/NewTransaction.kt | 1 - 43 files changed, 167 insertions(+), 144 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt b/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt index 15f98be512..fc593fdfce 100644 --- a/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt +++ b/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt @@ -1,12 +1,11 @@ package net.corda.core.node +import com.google.common.net.HostAndPort import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate -import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceType import net.corda.core.serialization.CordaSerializable -import org.bouncycastle.cert.X509CertificateHolder /** * Information for an advertised service including the service specific identity information. @@ -18,16 +17,17 @@ data class ServiceEntry(val info: ServiceInfo, val identity: PartyAndCertificate /** * Info about a network node that acts on behalf of some form of contract party. */ +// TODO We currently don't support multi-IP/multi-identity nodes, we only left slots in the data structures. @CordaSerializable -data class NodeInfo(val address: SingleMessageRecipient, - val legalIdentityAndCert: PartyAndCertificate, +data class NodeInfo(val addresses: List, + val legalIdentityAndCert: PartyAndCertificate, //TODO This field will be removed in future PR which gets rid of services. + val legalIdentitiesAndCerts: Set, val platformVersion: Int, var advertisedServices: List = emptyList(), - val physicalLocation: PhysicalLocation? = null) { + val worldMapLocation: WorldMapLocation? = null) { init { require(advertisedServices.none { it.identity == legalIdentityAndCert }) { "Service identities must be different from node legal identity" } } - val legalIdentity: Party get() = legalIdentityAndCert.party val notaryIdentity: Party @@ -35,7 +35,4 @@ data class NodeInfo(val address: SingleMessageRecipient, fun serviceIdentities(type: ServiceType): List { return advertisedServices.filter { it.info.type.isSubTypeOf(type) }.map { it.identity.party } } - fun servideIdentitiesAndCert(type: ServiceType): List { - return advertisedServices.filter { it.info.type.isSubTypeOf(type) }.map { it.identity } - } } diff --git a/core/src/main/kotlin/net/corda/core/node/PhysicalLocationStructures.kt b/core/src/main/kotlin/net/corda/core/node/PhysicalLocationStructures.kt index 8d48e3a27f..049845432f 100644 --- a/core/src/main/kotlin/net/corda/core/node/PhysicalLocationStructures.kt +++ b/core/src/main/kotlin/net/corda/core/node/PhysicalLocationStructures.kt @@ -43,15 +43,15 @@ data class WorldCoordinate(val latitude: Double, val longitude: Double) { * The [countryCode] field is a two letter ISO country code. */ @CordaSerializable -data class PhysicalLocation(val coordinate: WorldCoordinate, val description: String, val countryCode: String) +data class WorldMapLocation(val coordinate: WorldCoordinate, val description: String, val countryCode: String) /** * A simple lookup table of city names to their coordinates. Lookups are case insensitive. */ object CityDatabase { private val matcher = Regex("^([a-zA-Z- ]*) \\((..)\\)$") - private val caseInsensitiveLookups = HashMap() - val cityMap = HashMap() + private val caseInsensitiveLookups = HashMap() + val cityMap = HashMap() init { javaClass.getResourceAsStream("cities.txt").bufferedReader().useLines { lines -> @@ -60,7 +60,7 @@ object CityDatabase { val (name, lng, lat) = line.split('\t') val matchResult = matcher.matchEntire(name) ?: throw Exception("Could not parse line: $line") val (city, country) = matchResult.destructured - val location = PhysicalLocation(WorldCoordinate(lat.toDouble(), lng.toDouble()), city, country) + val location = WorldMapLocation(WorldCoordinate(lat.toDouble(), lng.toDouble()), city, country) caseInsensitiveLookups[city.toLowerCase()] = location cityMap[city] = location } diff --git a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt index 9756ba57fb..1198508986 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt @@ -69,7 +69,7 @@ class AttachmentSerializationTest { fun setUp() { mockNet = MockNetwork() server = mockNet.createNode(advertisedServices = ServiceInfo(NetworkMapService.type)) - client = mockNet.createNode(server.info.address) + client = mockNet.createNode(server.network.myAddress) client.disableDBCloseOnStop() // Otherwise the in-memory database may disappear (taking the checkpoint with it) while we reboot the client. mockNet.runNetwork() } @@ -149,7 +149,7 @@ class AttachmentSerializationTest { private fun rebootClientAndGetAttachmentContent(checkAttachmentsOnLoad: Boolean = true): String { client.stop() - client = mockNet.createNode(server.info.address, client.id, object : MockNetwork.Factory { + client = mockNet.createNode(server.network.myAddress, client.id, object : MockNetwork.Factory { override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, advertisedServices: Set, id: Int, overrideServices: Map?, entropyRoot: BigInteger): MockNetwork.MockNode { return object : MockNetwork.MockNode(config, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) { override fun startMessagingService(rpcOps: RPCOps) { diff --git a/core/src/test/kotlin/net/corda/flows/TxKeyFlowTests.kt b/core/src/test/kotlin/net/corda/flows/TxKeyFlowTests.kt index 7490819a48..c439f5ee41 100644 --- a/core/src/test/kotlin/net/corda/flows/TxKeyFlowTests.kt +++ b/core/src/test/kotlin/net/corda/flows/TxKeyFlowTests.kt @@ -27,8 +27,8 @@ class TxKeyFlowTests { // Set up values we'll need val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) - val aliceNode = mockNet.createPartyNode(notaryNode.info.address, ALICE.name) - val bobNode = mockNet.createPartyNode(notaryNode.info.address, BOB.name) + val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) + val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) val alice: Party = aliceNode.services.myInfo.legalIdentity val bob: Party = bobNode.services.myInfo.legalIdentity aliceNode.services.identityService.registerIdentity(bobNode.info.legalIdentityAndCert) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index ec669aea03..ae8ee4e2e9 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -6,6 +6,12 @@ from the previous milestone release. UNRELEASED ---------- +* Changes in ``NodeInfo``: + + * ``PhysicalLocation`` was renamed to ``WorldMapLocation`` to emphasise that it doesn't need to map to a truly physical + location of the node server. + * Slots for multiple IP addresses and ``legalIdentitiesAndCert``s were introduced. Addresses are no longer of type + ``SingleMessageRecipient``, but of ``HostAndPort``. Milestone 13 ---------- diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt index 39b61b1b09..33d75ce6ee 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt @@ -12,8 +12,6 @@ import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatedBy import net.corda.core.flows.InitiatingFlow import net.corda.core.identity.Party -import net.corda.core.identity.PartyAndCertificate -import net.corda.core.node.PluginServiceHub import net.corda.core.node.ServiceHub import net.corda.core.node.services.unconsumedStates import net.corda.core.serialization.CordaSerializable diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt index 4210c57ce6..b9b7abd4fa 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt @@ -31,8 +31,8 @@ class FxTransactionBuildTutorialTest { legalName = DUMMY_NOTARY.name, overrideServices = mapOf(notaryService to DUMMY_NOTARY_KEY), advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), notaryService)) - nodeA = mockNet.createPartyNode(notaryNode.info.address) - nodeB = mockNet.createPartyNode(notaryNode.info.address) + nodeA = mockNet.createPartyNode(notaryNode.network.myAddress) + nodeB = mockNet.createPartyNode(notaryNode.network.myAddress) nodeB.registerInitiatedFlow(ForeignExchangeRemoteFlow::class.java) } diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt index 17da495c0e..a997d06746 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt @@ -43,8 +43,8 @@ class WorkflowTransactionBuildTutorialTest { legalName = DUMMY_NOTARY.name, overrideServices = mapOf(Pair(notaryService, DUMMY_NOTARY_KEY)), advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), notaryService)) - nodeA = mockNet.createPartyNode(notaryNode.info.address) - nodeB = mockNet.createPartyNode(notaryNode.info.address) + nodeA = mockNet.createPartyNode(notaryNode.network.myAddress) + nodeB = mockNet.createPartyNode(notaryNode.network.myAddress) nodeA.registerInitiatedFlow(RecordCompletionFlow::class.java) } diff --git a/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt b/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt index dd812e30c2..e69f0947ee 100644 --- a/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt +++ b/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt @@ -37,8 +37,8 @@ class IssuerFlowTest { fun start() { mockNet = MockNetwork(threadPerNode = true) notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) - bankOfCordaNode = mockNet.createPartyNode(notaryNode.info.address, BOC.name) - bankClientNode = mockNet.createPartyNode(notaryNode.info.address, MEGA_CORP.name) + bankOfCordaNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name) + bankClientNode = mockNet.createPartyNode(notaryNode.network.myAddress, MEGA_CORP.name) } @After diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisMessagingComponent.kt b/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisMessagingComponent.kt index d77de310db..f31b75eaaa 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisMessagingComponent.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisMessagingComponent.kt @@ -6,6 +6,8 @@ import net.corda.core.crypto.toBase58String import net.corda.core.messaging.MessageRecipientGroup import net.corda.core.messaging.MessageRecipients import net.corda.core.messaging.SingleMessageRecipient +import net.corda.core.node.NodeInfo +import net.corda.core.node.services.ServiceType import net.corda.core.read import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SingletonSerializeAsToken @@ -34,18 +36,6 @@ abstract class ArtemisMessagingComponent : SingletonSerializeAsToken() { const val P2P_QUEUE = "p2p.inbound" const val NOTIFICATIONS_ADDRESS = "${INTERNAL_PREFIX}activemq.notifications" const val NETWORK_MAP_QUEUE = "${INTERNAL_PREFIX}networkmap" - - /** - * Assuming the passed in target address is actually an ArtemisAddress will extract the host and port of the node. This should - * only be used in unit tests and the internals of the messaging services to keep addressing opaque for the future. - * N.B. Marked as JvmStatic to allow use in the inherited classes. - */ - @JvmStatic - @VisibleForTesting - fun toHostAndPort(target: MessageRecipients): HostAndPort { - val addr = target as? ArtemisMessagingComponent.ArtemisPeerAddress ?: throw IllegalArgumentException("Not an Artemis address") - return addr.hostAndPort - } } interface ArtemisAddress : MessageRecipients { @@ -57,7 +47,7 @@ abstract class ArtemisMessagingComponent : SingletonSerializeAsToken() { } @CordaSerializable - data class NetworkMapAddress(override val hostAndPort: HostAndPort) : SingleMessageRecipient, ArtemisPeerAddress { + data class NetworkMapAddress(override val hostAndPort: HostAndPort) : ArtemisPeerAddress { override val queueName: String get() = NETWORK_MAP_QUEUE } @@ -116,4 +106,12 @@ abstract class ArtemisMessagingComponent : SingletonSerializeAsToken() { KeyStore.getInstance("JKS").load(it, config.trustStorePassword.toCharArray()) } } + + fun getArtemisPeerAddress(nodeInfo: NodeInfo): ArtemisPeerAddress { + return if (nodeInfo.advertisedServices.any { it.info.type == ServiceType.networkMap }) { + NetworkMapAddress(nodeInfo.addresses.first()) + } else { + NodeAddress.asPeer(nodeInfo.legalIdentity.owningKey, nodeInfo.addresses.first()) + } + } } diff --git a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt index 5e8c9516c6..35527d3f78 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt @@ -50,7 +50,7 @@ class BFTNotaryServiceTests { val notaryClusterAddresses = replicaIds.map { HostAndPort.fromParts("localhost", 11000 + it * 10) } replicaIds.forEach { replicaId -> mockNet.createNode( - node.info.address, + node.network.myAddress, advertisedServices = bftNotaryService, configOverrides = { whenever(it.bftReplicaId).thenReturn(replicaId) diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt index bca09c4a0f..49dfc8a119 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt @@ -16,6 +16,7 @@ import net.corda.node.services.network.NetworkMapService import net.corda.node.services.network.NetworkMapService.RegistrationRequest import net.corda.node.services.network.NodeRegistration import net.corda.node.utilities.AddOrRemove +import net.corda.testing.MOCK_HOST_AND_PORT import net.corda.testing.MOCK_VERSION_INFO import net.corda.testing.node.NodeBasedTest import net.corda.testing.node.SimpleNode @@ -69,7 +70,7 @@ class P2PSecurityTest : NodeBasedTest() { private fun SimpleNode.registerWithNetworkMap(registrationName: X500Name): ListenableFuture { val legalIdentity = getTestPartyAndCertificate(registrationName, identity.public) - val nodeInfo = NodeInfo(network.myAddress, legalIdentity, MOCK_VERSION_INFO.platformVersion) + val nodeInfo = NodeInfo(listOf(MOCK_HOST_AND_PORT), legalIdentity, setOf(legalIdentity), MOCK_VERSION_INFO.platformVersion) val registration = NodeRegistration(nodeInfo, System.currentTimeMillis(), AddOrRemove.ADD, Instant.MAX) val request = RegistrationRequest(registration.toWire(keyService, identity.public), network.myAddress) return network.sendRequest(NetworkMapService.REGISTER_TOPIC, request, networkMapNode.network.myAddress) diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index a0849bd84a..ac397c89d8 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -3,6 +3,7 @@ package net.corda.node.internal import com.codahale.metrics.MetricRegistry import com.google.common.annotations.VisibleForTesting import com.google.common.collect.MutableClassToInstanceMap +import com.google.common.net.HostAndPort import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.MoreExecutors import com.google.common.util.concurrent.SettableFuture @@ -58,6 +59,7 @@ import net.corda.node.utilities.AddOrRemove.ADD import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction +import net.corda.nodeapi.ArtemisMessagingComponent import org.apache.activemq.artemis.utils.ReusableLatch import org.bouncycastle.asn1.x500.X500Name import org.jetbrains.exposed.sql.Database @@ -158,7 +160,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, } } - open fun findMyLocation(): PhysicalLocation? { + open fun findMyLocation(): WorldMapLocation? { return configuration.myLegalName.locationOrNull?.let { CityDatabase[it] } } @@ -548,7 +550,9 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, private fun makeInfo(): NodeInfo { val advertisedServiceEntries = makeServiceEntries() val legalIdentity = obtainLegalIdentity() - return NodeInfo(network.myAddress, legalIdentity, platformVersion, advertisedServiceEntries, findMyLocation()) + val allIdentitiesSet = advertisedServiceEntries.map { it.identity }.toSet() + legalIdentity + val addresses = myAddresses() // TODO There is no support for multiple IP addresses yet. + return NodeInfo(addresses, legalIdentity, allIdentitiesSet, platformVersion, advertisedServiceEntries, findMyLocation()) } /** @@ -641,7 +645,8 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, require(networkMapAddress != null || NetworkMapService.type in advertisedServices.map { it.type }) { "Initial network map address must indicate a node that provides a network map service" } - val address = networkMapAddress ?: info.address + val address: SingleMessageRecipient = networkMapAddress ?: + network.getAddressOfParty(PartyInfo.Node(info)) as SingleMessageRecipient // Register for updates, even if we're the one running the network map. return sendNetworkMapRegistration(address).flatMap { (error) -> check(error == null) { "Unable to register with the network map service: $error" } @@ -660,6 +665,9 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, return network.sendRequest(NetworkMapService.REGISTER_TOPIC, request, networkMapAddress) } + /** Return list of node's addresses. It's overridden in MockNetwork as we don't have real addresses for MockNodes. */ + protected abstract fun myAddresses(): List + /** This is overriden by the mock node implementation to enable operation without any network map service */ protected open fun noNetworkMapConfigured(): ListenableFuture { // TODO: There should be a consistent approach to configuration error exceptions. diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index 2ff859c56d..347f35edd2 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -32,6 +32,7 @@ import net.corda.node.services.transactions.RaftUniquenessProvider import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.node.utilities.AddressUtils import net.corda.node.utilities.AffinityExecutor +import net.corda.nodeapi.ArtemisMessagingComponent import net.corda.nodeapi.ArtemisMessagingComponent.Companion.IP_REQUEST_PREFIX import net.corda.nodeapi.ArtemisMessagingComponent.Companion.PEER_USER import net.corda.nodeapi.ArtemisMessagingComponent.NetworkMapAddress @@ -274,6 +275,11 @@ open class Node(override val configuration: FullNodeConfiguration, } } + override fun myAddresses(): List { + val address = network.myAddress as ArtemisMessagingComponent.ArtemisPeerAddress + return listOf(address.hostAndPort) + } + /** * If the node is persisting to an embedded H2 database, then expose this via TCP with a JDBC URL of the form: * jdbc:h2:tcp://:/node diff --git a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt index 5ad98740a3..abcda505f4 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt @@ -1,6 +1,7 @@ package net.corda.node.services.api import com.google.common.annotations.VisibleForTesting +import com.google.common.net.HostAndPort import com.google.common.util.concurrent.ListenableFuture import net.corda.core.flows.FlowInitiator import net.corda.core.flows.FlowLogic diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt index 5b2d5f9c3c..cf5214e217 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt @@ -298,12 +298,8 @@ class ArtemisMessagingServer(override val config: NodeConfiguration, fun deployBridgeToPeer(nodeInfo: NodeInfo) { log.debug("Deploying bridge for $queueName to $nodeInfo") - val address = nodeInfo.address - if (address is ArtemisPeerAddress) { - deployBridge(queueName, address.hostAndPort, nodeInfo.legalIdentity.name) - } else { - log.error("Don't know how to deal with $address for queue $queueName") - } + val address = nodeInfo.addresses.first() // TODO Load balancing. + deployBridge(queueName, address, nodeInfo.legalIdentity.name) } when { @@ -342,7 +338,7 @@ class ArtemisMessagingServer(override val config: NodeConfiguration, */ private fun updateBridgesOnNetworkChange(change: MapChange) { fun gatherAddresses(node: NodeInfo): Sequence { - val peerAddress = node.address as ArtemisPeerAddress + val peerAddress = getArtemisPeerAddress(node) val addresses = mutableListOf(peerAddress) node.advertisedServices.mapTo(addresses) { NodeAddress.asService(it.identity.owningKey, peerAddress.hostAndPort) } return addresses.asSequence() diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt index 56d56b8746..9bc17e894c 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt @@ -564,11 +564,10 @@ class NodeMessagingClient(override val config: NodeConfiguration, } } - override fun getAddressOfParty(partyInfo: PartyInfo): MessageRecipients { return when (partyInfo) { - is PartyInfo.Node -> partyInfo.node.address - is PartyInfo.Service -> ArtemisMessagingComponent.ServiceAddress(partyInfo.service.identity.owningKey) + is PartyInfo.Node -> getArtemisPeerAddress(partyInfo.node) + is PartyInfo.Service -> ServiceAddress(partyInfo.service.identity.owningKey) } } } diff --git a/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt b/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt index e1b2664b90..f25c210c59 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt @@ -134,7 +134,9 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach override fun deregisterForUpdates(network: MessagingService, service: NodeInfo): ListenableFuture { // Fetch the network map and register for updates at the same time val req = NetworkMapService.SubscribeRequest(false, network.myAddress) - val future = network.sendRequest(NetworkMapService.SUBSCRIPTION_TOPIC, req, service.address).map { + // `network.getAddressOfParty(partyInfo)` is a work-around for MockNetwork and InMemoryMessaging to get rid of SingleMessageRecipient in NodeInfo. + val address = network.getAddressOfParty(PartyInfo.Node(service)) + val future = network.sendRequest(NetworkMapService.SUBSCRIPTION_TOPIC, req, address).map { if (it.confirmed) Unit else throw NetworkCacheError.DeregistrationFailed() } _registrationFuture.setFuture(future) diff --git a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt index 035a1a506c..ce8319ebcb 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt @@ -273,11 +273,11 @@ abstract class AbstractNetworkMapService(services: ServiceHubInternal, // subscribers when (change.type) { ADD -> { - logger.info("Added node ${node.address} to network map") + logger.info("Added node ${node.addresses} to network map") services.networkMapCache.addNode(change.node) } REMOVE -> { - logger.info("Removed node ${node.address} from network map") + logger.info("Removed node ${node.addresses} from network map") services.networkMapCache.removeNode(change.node) } } diff --git a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt index 831f0b9d16..37629331f6 100644 --- a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt @@ -59,8 +59,8 @@ class CordaRPCOpsImplTest { fun setup() { mockNet = MockNetwork() val networkMap = mockNet.createNode(advertisedServices = ServiceInfo(NetworkMapService.type)) - aliceNode = mockNet.createNode(networkMapAddress = networkMap.info.address) - notaryNode = mockNet.createNode(advertisedServices = ServiceInfo(SimpleNotaryService.type), networkMapAddress = networkMap.info.address) + aliceNode = mockNet.createNode(networkMapAddress = networkMap.network.myAddress) + notaryNode = mockNet.createNode(advertisedServices = ServiceInfo(SimpleNotaryService.type), networkMapAddress = networkMap.network.myAddress) rpc = CordaRPCOpsImpl(aliceNode.services, aliceNode.smm, aliceNode.database) CURRENT_RPC_CONTEXT.set(RpcContext(User("user", "pwd", permissions = setOf( startFlowPermission(), diff --git a/node/src/test/kotlin/net/corda/node/messaging/AttachmentTests.kt b/node/src/test/kotlin/net/corda/node/messaging/AttachmentTests.kt index 78408f57dd..9331aeaf33 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/AttachmentTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/AttachmentTests.kt @@ -114,7 +114,7 @@ class AttachmentTests { } } }, true, null, null, ServiceInfo(NetworkMapService.type), ServiceInfo(SimpleNotaryService.type)) - val n1 = mockNet.createNode(n0.info.address) + val n1 = mockNet.createNode(n0.network.myAddress) val attachment = fakeAttachment() // Insert an attachment into node zero's store directly. diff --git a/node/src/test/kotlin/net/corda/node/messaging/InMemoryMessagingTests.kt b/node/src/test/kotlin/net/corda/node/messaging/InMemoryMessagingTests.kt index bf6c2755fe..795f5d29fe 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/InMemoryMessagingTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/InMemoryMessagingTests.kt @@ -34,15 +34,15 @@ class InMemoryMessagingTests { @Test fun basics() { val node1 = mockNet.createNode(advertisedServices = ServiceInfo(NetworkMapService.type)) - val node2 = mockNet.createNode(networkMapAddress = node1.info.address) - val node3 = mockNet.createNode(networkMapAddress = node1.info.address) + val node2 = mockNet.createNode(networkMapAddress = node1.network.myAddress) + val node3 = mockNet.createNode(networkMapAddress = node1.network.myAddress) val bits = "test-content".toByteArray() var finalDelivery: Message? = null with(node2) { node2.network.addMessageHandler { msg, _ -> - node2.network.send(msg, node3.info.address) + node2.network.send(msg, node3.network.myAddress) } } @@ -53,7 +53,7 @@ class InMemoryMessagingTests { } // Node 1 sends a message and it should end up in finalDelivery, after we run the network - node1.network.send(node1.network.createMessage("test.topic", DEFAULT_SESSION_ID, bits), node2.info.address) + node1.network.send(node1.network.createMessage("test.topic", DEFAULT_SESSION_ID, bits), node2.network.myAddress) mockNet.runNetwork(rounds = 1) @@ -63,8 +63,8 @@ class InMemoryMessagingTests { @Test fun broadcast() { val node1 = mockNet.createNode(advertisedServices = ServiceInfo(NetworkMapService.type)) - val node2 = mockNet.createNode(networkMapAddress = node1.info.address) - val node3 = mockNet.createNode(networkMapAddress = node1.info.address) + val node2 = mockNet.createNode(networkMapAddress = node1.network.myAddress) + val node3 = mockNet.createNode(networkMapAddress = node1.network.myAddress) val bits = "test-content".toByteArray() @@ -82,7 +82,7 @@ class InMemoryMessagingTests { @Test fun `skip unhandled messages`() { val node1 = mockNet.createNode(advertisedServices = ServiceInfo(NetworkMapService.type)) - val node2 = mockNet.createNode(networkMapAddress = node1.info.address) + val node2 = mockNet.createNode(networkMapAddress = node1.network.myAddress) var received: Int = 0 node1.network.addMessageHandler("valid_message") { _, _ -> diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index 6256e5ec3e..493fe77ce1 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -127,8 +127,8 @@ class TwoPartyTradeFlowTests { ledger { val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) - val aliceNode = mockNet.createPartyNode(notaryNode.info.address, ALICE.name) - val bobNode = mockNet.createPartyNode(notaryNode.info.address, BOB.name) + val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) + val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) aliceNode.disableDBCloseOnStop() bobNode.disableDBCloseOnStop() @@ -173,13 +173,13 @@ class TwoPartyTradeFlowTests { fun `shutdown and restore`() { ledger { val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) - val aliceNode = mockNet.createPartyNode(notaryNode.info.address, ALICE.name) - var bobNode = mockNet.createPartyNode(notaryNode.info.address, BOB.name) + val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) + var bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) aliceNode.disableDBCloseOnStop() bobNode.disableDBCloseOnStop() val bobAddr = bobNode.network.myAddress as InMemoryMessagingNetwork.PeerHandle - val networkMapAddr = notaryNode.info.address + val networkMapAddr = notaryNode.network.myAddress mockNet.runNetwork() // Clear network map registration messages @@ -291,8 +291,8 @@ class TwoPartyTradeFlowTests { @Test fun `check dependencies of sale asset are resolved`() { val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) - val aliceNode = makeNodeWithTracking(notaryNode.info.address, ALICE.name) - val bobNode = makeNodeWithTracking(notaryNode.info.address, BOB.name) + val aliceNode = makeNodeWithTracking(notaryNode.network.myAddress, ALICE.name) + val bobNode = makeNodeWithTracking(notaryNode.network.myAddress, BOB.name) ledger(aliceNode.services) { @@ -390,8 +390,8 @@ class TwoPartyTradeFlowTests { @Test fun `track works`() { val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) - val aliceNode = makeNodeWithTracking(notaryNode.info.address, ALICE.name) - val bobNode = makeNodeWithTracking(notaryNode.info.address, BOB.name) + val aliceNode = makeNodeWithTracking(notaryNode.network.myAddress, ALICE.name) + val bobNode = makeNodeWithTracking(notaryNode.network.myAddress, BOB.name) ledger(aliceNode.services) { @@ -528,8 +528,8 @@ class TwoPartyTradeFlowTests { expectedMessageSubstring: String ) { val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) - val aliceNode = mockNet.createPartyNode(notaryNode.info.address, ALICE.name) - val bobNode = mockNet.createPartyNode(notaryNode.info.address, BOB.name) + val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) + val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) val issuer = MEGA_CORP.ref(1, 2, 3) val bobsBadCash = bobNode.database.transaction { diff --git a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt index 84954fa218..44486779d1 100644 --- a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt @@ -37,9 +37,9 @@ class NotaryChangeTests { oldNotaryNode = mockNet.createNode( legalName = DUMMY_NOTARY.name, advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(SimpleNotaryService.type))) - clientNodeA = mockNet.createNode(networkMapAddress = oldNotaryNode.info.address) - clientNodeB = mockNet.createNode(networkMapAddress = oldNotaryNode.info.address) - newNotaryNode = mockNet.createNode(networkMapAddress = oldNotaryNode.info.address, advertisedServices = ServiceInfo(SimpleNotaryService.type)) + clientNodeA = mockNet.createNode(networkMapAddress = oldNotaryNode.network.myAddress) + clientNodeB = mockNet.createNode(networkMapAddress = oldNotaryNode.network.myAddress) + newNotaryNode = mockNet.createNode(networkMapAddress = oldNotaryNode.network.myAddress, advertisedServices = ServiceInfo(SimpleNotaryService.type)) mockNet.runNetwork() // Clear network map registration messages } diff --git a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt index 10a31f2cbe..f818364974 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt @@ -94,8 +94,8 @@ class ScheduledFlowTests { notaryNode = mockNet.createNode( legalName = DUMMY_NOTARY.name, advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(ValidatingNotaryService.type))) - nodeA = mockNet.createNode(notaryNode.info.address, start = false) - nodeB = mockNet.createNode(notaryNode.info.address, start = false) + nodeA = mockNet.createNode(notaryNode.network.myAddress, start = false) + nodeB = mockNet.createNode(notaryNode.network.myAddress, start = false) mockNet.startNodes() } diff --git a/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt index 47a45d8e9b..300f2e95f7 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt @@ -186,8 +186,8 @@ abstract class AbstractNetworkMapServiceTest } private fun MockNode.fetchMap(subscribe: Boolean = false, ifChangedSinceVersion: Int? = null): List { - val request = FetchMapRequest(subscribe, ifChangedSinceVersion, info.address) - val response = services.networkService.sendRequest(FETCH_TOPIC, request, mapServiceNode.info.address) + val request = FetchMapRequest(subscribe, ifChangedSinceVersion, network.myAddress) + val response = services.networkService.sendRequest(FETCH_TOPIC, request, mapServiceNode.network.myAddress) mockNet.runNetwork() return response.getOrThrow().nodes?.map { it.toChanged() } ?: emptyList() } @@ -198,8 +198,8 @@ abstract class AbstractNetworkMapServiceTest } private fun MockNode.identityQuery(): NodeInfo? { - val request = QueryIdentityRequest(info.legalIdentityAndCert, info.address) - val response = services.networkService.sendRequest(QUERY_TOPIC, request, mapServiceNode.info.address) + val request = QueryIdentityRequest(info.legalIdentityAndCert, network.myAddress) + val response = services.networkService.sendRequest(QUERY_TOPIC, request, mapServiceNode.network.myAddress) mockNet.runNetwork() return response.getOrThrow().node } @@ -216,39 +216,39 @@ abstract class AbstractNetworkMapServiceTest } val expires = Instant.now() + NetworkMapService.DEFAULT_EXPIRATION_PERIOD val nodeRegistration = NodeRegistration(info, distinctSerial, addOrRemove, expires) - val request = RegistrationRequest(nodeRegistration.toWire(services.keyManagementService, services.legalIdentityKey), info.address) - val response = services.networkService.sendRequest(REGISTER_TOPIC, request, mapServiceNode.info.address) + val request = RegistrationRequest(nodeRegistration.toWire(services.keyManagementService, services.legalIdentityKey), network.myAddress) + val response = services.networkService.sendRequest(REGISTER_TOPIC, request, mapServiceNode.network.myAddress) mockNet.runNetwork() return response } private fun MockNode.subscribe(): List { - val request = SubscribeRequest(true, info.address) + val request = SubscribeRequest(true, network.myAddress) val updates = BlockingArrayQueue() services.networkService.addMessageHandler(PUSH_TOPIC, DEFAULT_SESSION_ID) { message, _ -> updates += message.data.deserialize() } - val response = services.networkService.sendRequest(SUBSCRIPTION_TOPIC, request, mapServiceNode.info.address) + val response = services.networkService.sendRequest(SUBSCRIPTION_TOPIC, request, mapServiceNode.network.myAddress) mockNet.runNetwork() assertThat(response.getOrThrow().confirmed).isTrue() return updates } private fun MockNode.unsubscribe() { - val request = SubscribeRequest(false, info.address) - val response = services.networkService.sendRequest(SUBSCRIPTION_TOPIC, request, mapServiceNode.info.address) + val request = SubscribeRequest(false, network.myAddress) + val response = services.networkService.sendRequest(SUBSCRIPTION_TOPIC, request, mapServiceNode.network.myAddress) mockNet.runNetwork() assertThat(response.getOrThrow().confirmed).isTrue() } private fun MockNode.ackUpdate(mapVersion: Int) { val request = UpdateAcknowledge(mapVersion, services.networkService.myAddress) - services.networkService.send(PUSH_ACK_TOPIC, DEFAULT_SESSION_ID, request, mapServiceNode.info.address) + services.networkService.send(PUSH_ACK_TOPIC, DEFAULT_SESSION_ID, request, mapServiceNode.network.myAddress) mockNet.runNetwork() } private fun addNewNodeToNetworkMap(legalName: X500Name): MockNode { - val node = mockNet.createNode(networkMapAddress = mapServiceNode.info.address, legalName = legalName) + val node = mockNet.createNode(networkMapAddress = mapServiceNode.network.myAddress, legalName = legalName) mockNet.runNetwork() lastSerial = System.currentTimeMillis() return node diff --git a/node/src/test/kotlin/net/corda/node/services/network/InMemoryNetworkMapCacheTest.kt b/node/src/test/kotlin/net/corda/node/services/network/InMemoryNetworkMapCacheTest.kt index a341420963..3a5f677305 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/InMemoryNetworkMapCacheTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/InMemoryNetworkMapCacheTest.kt @@ -16,7 +16,7 @@ class InMemoryNetworkMapCacheTest { @Test fun registerWithNetwork() { val (n0, n1) = mockNet.createTwoNodes() - val future = n1.services.networkMapCache.addMapService(n1.network, n0.info.address, false, null) + val future = n1.services.networkMapCache.addMapService(n1.network, n0.network.myAddress, false, null) mockNet.runNetwork() future.getOrThrow() } diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt index 9e4ea74112..2497fcd06f 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt @@ -81,8 +81,8 @@ class FlowFrameworkTests { val overrideServices = mapOf(Pair(notaryService, notaryKeyPair)) // Note that these notaries don't operate correctly as they don't share their state. They are only used for testing // service addressing. - notary1 = mockNet.createNotaryNode(networkMapAddr = node1.services.myInfo.address, overrideServices = overrideServices, serviceName = notaryService.name) - notary2 = mockNet.createNotaryNode(networkMapAddr = node1.services.myInfo.address, overrideServices = overrideServices, serviceName = notaryService.name) + notary1 = mockNet.createNotaryNode(networkMapAddr = node1.network.myAddress, overrideServices = overrideServices, serviceName = notaryService.name) + notary2 = mockNet.createNotaryNode(networkMapAddr = node1.network.myAddress, overrideServices = overrideServices, serviceName = notaryService.name) mockNet.messagingNetwork.receivedMessages.toSessionTransfers().forEach { sessionTransfers += it } mockNet.runNetwork() @@ -148,7 +148,7 @@ class FlowFrameworkTests { @Test fun `flow added before network map does run after init`() { - val node3 = mockNet.createNode(node1.info.address) //create vanilla node + val node3 = mockNet.createNode(node1.network.myAddress) //create vanilla node val flow = NoOpFlow() node3.services.startFlow(flow) assertEquals(false, flow.flowStarted) // Not started yet as no network activity has been allowed yet @@ -158,14 +158,14 @@ class FlowFrameworkTests { @Test fun `flow added before network map will be init checkpointed`() { - var node3 = mockNet.createNode(node1.info.address) //create vanilla node + var node3 = mockNet.createNode(node1.network.myAddress) //create vanilla node val flow = NoOpFlow() node3.services.startFlow(flow) assertEquals(false, flow.flowStarted) // Not started yet as no network activity has been allowed yet node3.disableDBCloseOnStop() node3.stop() - node3 = mockNet.createNode(node1.info.address, forcedID = node3.id) + node3 = mockNet.createNode(node1.network.myAddress, forcedID = node3.id) val restoredFlow = node3.getSingleFlow().first assertEquals(false, restoredFlow.flowStarted) // Not started yet as no network activity has been allowed yet mockNet.runNetwork() // Allow network map messages to flow @@ -175,7 +175,7 @@ class FlowFrameworkTests { node3.stop() // Now it is completed the flow should leave no Checkpoint. - node3 = mockNet.createNode(node1.info.address, forcedID = node3.id) + node3 = mockNet.createNode(node1.network.myAddress, forcedID = node3.id) mockNet.runNetwork() // Allow network map messages to flow node3.smm.executor.flush() assertTrue(node3.smm.findStateMachines(NoOpFlow::class.java).isEmpty()) @@ -201,7 +201,7 @@ class FlowFrameworkTests { var sentCount = 0 mockNet.messagingNetwork.sentMessages.toSessionTransfers().filter { it.isPayloadTransfer }.forEach { sentCount++ } - val node3 = mockNet.createNode(node1.info.address) + val node3 = mockNet.createNode(node1.network.myAddress) val secondFlow = node3.registerFlowFactory(PingPongFlow::class) { PingPongFlow(it, payload2) } mockNet.runNetwork() @@ -218,7 +218,7 @@ class FlowFrameworkTests { node2.database.transaction { assertEquals(1, node2.checkpointStorage.checkpoints().size) // confirm checkpoint } - val node2b = mockNet.createNode(node1.info.address, node2.id, advertisedServices = *node2.advertisedServices.toTypedArray()) + val node2b = mockNet.createNode(node1.network.myAddress, node2.id, advertisedServices = *node2.advertisedServices.toTypedArray()) node2.manuallyCloseDB() val (firstAgain, fut1) = node2b.getSingleFlow() // Run the network which will also fire up the second flow. First message should get deduped. So message data stays in sync. @@ -245,7 +245,7 @@ class FlowFrameworkTests { @Test fun `sending to multiple parties`() { - val node3 = mockNet.createNode(node1.info.address) + val node3 = mockNet.createNode(node1.network.myAddress) mockNet.runNetwork() node2.registerFlowFactory(SendFlow::class) { ReceiveFlow(it).nonTerminating() } node3.registerFlowFactory(SendFlow::class) { ReceiveFlow(it).nonTerminating() } @@ -277,7 +277,7 @@ class FlowFrameworkTests { @Test fun `receiving from multiple parties`() { - val node3 = mockNet.createNode(node1.info.address) + val node3 = mockNet.createNode(node1.network.myAddress) mockNet.runNetwork() val node2Payload = "Test 1" val node3Payload = "Test 2" @@ -457,7 +457,7 @@ class FlowFrameworkTests { @Test fun `FlowException propagated in invocation chain`() { - val node3 = mockNet.createNode(node1.info.address) + val node3 = mockNet.createNode(node1.network.myAddress) mockNet.runNetwork() node3.registerFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Chain") } } @@ -471,7 +471,7 @@ class FlowFrameworkTests { @Test fun `FlowException thrown and there is a 3rd unrelated party flow`() { - val node3 = mockNet.createNode(node1.info.address) + val node3 = mockNet.createNode(node1.network.myAddress) mockNet.runNetwork() // Node 2 will send its payload and then block waiting for the receive from node 1. Meanwhile node 1 will move @@ -675,7 +675,7 @@ class FlowFrameworkTests { private inline fun > MockNode.restartAndGetRestoredFlow(networkMapNode: MockNode? = null): P { disableDBCloseOnStop() // Handover DB to new node copy stop() - val newNode = mockNet.createNode(networkMapNode?.info?.address, id, advertisedServices = *advertisedServices.toTypedArray()) + val newNode = mockNet.createNode(networkMapNode?.network?.myAddress, id, advertisedServices = *advertisedServices.toTypedArray()) newNode.acceptableLiveFiberCountOnStop = 1 manuallyCloseDB() mockNet.runNetwork() // allow NetworkMapService messages to stabilise and thus start the state machine diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt index 039ca933b6..61c708f875 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt @@ -35,7 +35,7 @@ class NotaryServiceTests { notaryNode = mockNet.createNode( legalName = DUMMY_NOTARY.name, advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(SimpleNotaryService.type))) - clientNode = mockNet.createNode(networkMapAddress = notaryNode.info.address) + clientNode = mockNet.createNode(networkMapAddress = notaryNode.network.myAddress) mockNet.runNetwork() // Clear network map registration messages } diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt index aa8a417d81..d2d9080483 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt @@ -33,7 +33,7 @@ class ValidatingNotaryServiceTests { legalName = DUMMY_NOTARY.name, advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(ValidatingNotaryService.type)) ) - clientNode = mockNet.createNode(networkMapAddress = notaryNode.info.address) + clientNode = mockNet.createNode(networkMapAddress = notaryNode.network.myAddress) mockNet.runNetwork() // Clear network map registration messages } diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt index 4f25c1af8c..9f2eb05981 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt @@ -213,7 +213,7 @@ class NodeInterestRatesTest { fun `network tearoff`() { val mockNet = MockNetwork() val n1 = mockNet.createNotaryNode() - val n2 = mockNet.createNode(n1.info.address, advertisedServices = ServiceInfo(NodeInterestRates.Oracle.type)) + val n2 = mockNet.createNode(n1.network.myAddress, advertisedServices = ServiceInfo(NodeInterestRates.Oracle.type)) n2.registerInitiatedFlow(NodeInterestRates.FixQueryHandler::class.java) n2.registerInitiatedFlow(NodeInterestRates.FixSignHandler::class.java) n2.database.transaction { diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/flows/UpdateBusinessDayFlow.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/flows/UpdateBusinessDayFlow.kt index cce9a84d78..d15c86bcec 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/flows/UpdateBusinessDayFlow.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/flows/UpdateBusinessDayFlow.kt @@ -64,4 +64,4 @@ object UpdateBusinessDayFlow { send(recipient.legalIdentity, UpdateBusinessDayMessage(date)) } } -} +} \ No newline at end of file diff --git a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt index 89f4b7dfa9..9eec30633d 100644 --- a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt +++ b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt @@ -7,7 +7,7 @@ import net.corda.core.flatMap import net.corda.core.flows.FlowLogic import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.CityDatabase -import net.corda.core.node.PhysicalLocation +import net.corda.core.node.WorldMapLocation import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.containsType import net.corda.core.utilities.DUMMY_MAP @@ -57,7 +57,7 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, advertisedServices: Set, id: Int, overrideServices: Map?, entropyRoot: BigInteger) : MockNetwork.MockNode(config, mockNet, networkMapAddress, advertisedServices, id, overrideServices, entropyRoot) { - override fun findMyLocation(): PhysicalLocation? { + override fun findMyLocation(): WorldMapLocation? { return configuration.myLegalName.locationOrNull?.let { CityDatabase[it] } } } @@ -80,7 +80,7 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, fun createAll(): List { return bankLocations.mapIndexed { i, _ -> // Use deterministic seeds so the simulation is stable. Needed so that party owning keys are stable. - mockNet.createNode(networkMap.info.address, start = false, nodeFactory = this, entropyRoot = BigInteger.valueOf(i.toLong())) as SimulatedNode + mockNet.createNode(networkMap.network.myAddress, start = false, nodeFactory = this, entropyRoot = BigInteger.valueOf(i.toLong())) as SimulatedNode } } } @@ -158,10 +158,10 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, val networkMap: SimulatedNode = mockNet.createNode(null, nodeFactory = NetworkMapNodeFactory, advertisedServices = ServiceInfo(NetworkMapService.type)) as SimulatedNode val notary: SimulatedNode - = mockNet.createNode(networkMap.info.address, nodeFactory = NotaryNodeFactory, advertisedServices = ServiceInfo(SimpleNotaryService.type)) as SimulatedNode - val regulators: List = listOf(mockNet.createNode(networkMap.info.address, start = false, nodeFactory = RegulatorFactory) as SimulatedNode) + = mockNet.createNode(networkMap.network.myAddress, nodeFactory = NotaryNodeFactory, advertisedServices = ServiceInfo(SimpleNotaryService.type)) as SimulatedNode + val regulators: List = listOf(mockNet.createNode(networkMap.network.myAddress, start = false, nodeFactory = RegulatorFactory) as SimulatedNode) val ratesOracle: SimulatedNode - = mockNet.createNode(networkMap.info.address, start = false, nodeFactory = RatesOracleFactory, advertisedServices = ServiceInfo(NodeInterestRates.Oracle.type)) as SimulatedNode + = mockNet.createNode(networkMap.network.myAddress, start = false, nodeFactory = RatesOracleFactory, advertisedServices = ServiceInfo(NodeInterestRates.Oracle.type)) as SimulatedNode // All nodes must be in one of these two lists for the purposes of the visualiser tool. val serviceProviders: List = listOf(notary, ratesOracle, networkMap) diff --git a/test-utils/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt b/test-utils/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt index 5ac11f2139..77ef496a0c 100644 --- a/test-utils/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt +++ b/test-utils/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt @@ -26,13 +26,13 @@ class DriverTests { private val executorService: ScheduledExecutorService = Executors.newScheduledThreadPool(2) private fun nodeMustBeUp(handleFuture: ListenableFuture) = handleFuture.getOrThrow().apply { - val hostAndPort = ArtemisMessagingComponent.toHostAndPort(nodeInfo.address) + val hostAndPort = nodeInfo.addresses.first() // Check that the port is bound addressMustBeBound(executorService, hostAndPort, (this as? NodeHandle.OutOfProcess)?.process) } private fun nodeMustBeDown(handle: NodeHandle) { - val hostAndPort = ArtemisMessagingComponent.toHostAndPort(handle.nodeInfo.address) + val hostAndPort = handle.nodeInfo.addresses.first() // Check that the port is bound addressMustNotBeBound(executorService, hostAndPort) } diff --git a/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt b/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt index febd95d3dd..6687956eee 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt @@ -90,6 +90,7 @@ val MOCK_IDENTITIES = listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_NOTAR val MOCK_IDENTITY_SERVICE: IdentityService get() = InMemoryIdentityService(MOCK_IDENTITIES, emptyMap(), DUMMY_CA.certificate.cert) val MOCK_VERSION_INFO = VersionInfo(1, "Mock release", "Mock revision", "Mock Vendor") +val MOCK_HOST_AND_PORT = HostAndPort.fromParts("mockHost", 30000) fun generateStateRef() = StateRef(SecureHash.randomSHA256(), 0) diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt b/test-utils/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt index ffe15ee54a..b5546e9fc4 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt @@ -82,6 +82,8 @@ class InMemoryMessagingNetwork( // Holds the mapping from services to peers advertising the service. private val serviceToPeersMapping = HashMap>() + // Holds the mapping from node's X500Name to PeerHandle. + private val peersMapping = HashMap() @Suppress("unused") // Used by the visualiser tool. /** A stream of (sender, message, recipients) triples */ @@ -127,10 +129,12 @@ class InMemoryMessagingNetwork( id: Int, executor: AffinityExecutor, advertisedServices: List, - description: X500Name? = null, + description: X500Name = X509Utilities.getX509Name("In memory node $id","London","demo@r3.com",null), database: Database) : MessagingServiceBuilder { - return Builder(manuallyPumped, PeerHandle(id, description ?: X509Utilities.getX509Name("In memory node $id","London","demo@r3.com",null)), advertisedServices.map(::ServiceHandle), executor, database = database) + val peerHandle = PeerHandle(id, description) + peersMapping[peerHandle.description] = peerHandle // Assume that the same name - the same entity in MockNetwork. + return Builder(manuallyPumped, peerHandle, advertisedServices.map(::ServiceHandle), executor, database = database) } interface LatencyCalculator { @@ -330,7 +334,7 @@ class InMemoryMessagingNetwork( override fun getAddressOfParty(partyInfo: PartyInfo): MessageRecipients { return when (partyInfo) { - is PartyInfo.Node -> partyInfo.node.address + is PartyInfo.Node -> peersMapping[partyInfo.party.name] ?: throw IllegalArgumentException("No MockNode for party ${partyInfo.party.name}") is PartyInfo.Service -> ServiceHandle(partyInfo.service) } } diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt index 47bd3bf6e2..84e3c3e745 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt @@ -1,9 +1,9 @@ package net.corda.testing.node import co.paralleluniverse.common.util.VisibleForTesting +import com.google.common.net.HostAndPort import net.corda.core.crypto.entropyToKeyPair import net.corda.core.identity.Party -import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.NodeInfo import net.corda.core.node.services.NetworkMapCache import net.corda.core.utilities.getTestPartyAndCertificate @@ -21,15 +21,15 @@ class MockNetworkMapCache : InMemoryNetworkMapCache() { private companion object { val BANK_C = getTestPartyAndCertificate(getTestX509Name("Bank C"), entropyToKeyPair(BigInteger.valueOf(1000)).public) val BANK_D = getTestPartyAndCertificate(getTestX509Name("Bank D"), entropyToKeyPair(BigInteger.valueOf(2000)).public) + val BANK_C_ADDR: HostAndPort = HostAndPort.fromParts("bankC", 8080) + val BANK_D_ADDR: HostAndPort = HostAndPort.fromParts("bankD", 8080) } override val changed: Observable = PublishSubject.create() - data class MockAddress(val id: String) : SingleMessageRecipient - init { - val mockNodeA = NodeInfo(MockAddress("bankC:8080"), BANK_C, MOCK_VERSION_INFO.platformVersion) - val mockNodeB = NodeInfo(MockAddress("bankD:8080"), BANK_D, MOCK_VERSION_INFO.platformVersion) + val mockNodeA = NodeInfo(listOf(BANK_C_ADDR), BANK_C, setOf(BANK_C), MOCK_VERSION_INFO.platformVersion) + val mockNodeB = NodeInfo(listOf(BANK_D_ADDR), BANK_D, setOf(BANK_D), MOCK_VERSION_INFO.platformVersion) registeredNodes[mockNodeA.legalIdentity.owningKey] = mockNodeA registeredNodes[mockNodeB.legalIdentity.owningKey] = mockNodeB runWithoutMapService() diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt index 6a068d60db..44e762f1f8 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -2,6 +2,7 @@ package net.corda.testing.node import com.google.common.jimfs.Configuration.unix import com.google.common.jimfs.Jimfs +import com.google.common.net.HostAndPort import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import com.nhaarman.mockito_kotlin.whenever @@ -14,7 +15,7 @@ import net.corda.core.messaging.MessageRecipients import net.corda.core.messaging.RPCOps import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.CordaPluginRegistry -import net.corda.core.node.PhysicalLocation +import net.corda.core.node.WorldMapLocation import net.corda.core.node.ServiceEntry import net.corda.core.node.services.* import net.corda.core.utilities.DUMMY_NOTARY_KEY @@ -222,12 +223,14 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, override fun noNetworkMapConfigured(): ListenableFuture = Futures.immediateFuture(Unit) // There is no need to slow down the unit tests by initialising CityDatabase - override fun findMyLocation(): PhysicalLocation? = null + override fun findMyLocation(): WorldMapLocation? = null override fun makeUniquenessProvider(type: ServiceType): UniquenessProvider = InMemoryUniquenessProvider() override fun makeTransactionVerifierService() = InMemoryTransactionVerifierService(1) + override fun myAddresses(): List = listOf(HostAndPort.fromHost("mockHost")) + override fun start(): MockNode { super.start() mockNet.identities.add(info.legalIdentityAndCert) @@ -242,7 +245,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, // This does not indirect through the NodeInfo object so it can be called before the node is started. // It is used from the network visualiser tool. - @Suppress("unused") val place: PhysicalLocation get() = findMyLocation()!! + @Suppress("unused") val place: WorldMapLocation get() = findMyLocation()!! fun pumpReceive(block: Boolean = false): InMemoryMessagingNetwork.MessageTransfer? { return (network as InMemoryMessagingNetwork.InMemoryMessaging).pumpReceive(block) @@ -351,7 +354,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, null return Pair( createNode(null, -1, nodeFactory, true, firstNodeName, notaryOverride, BigInteger.valueOf(random63BitValue()), ServiceInfo(NetworkMapService.type), notaryServiceInfo), - createNode(nodes[0].info.address, -1, nodeFactory, true, secondNodeName) + createNode(nodes[0].network.myAddress, -1, nodeFactory, true, secondNodeName) ) } @@ -374,11 +377,12 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, else null val mapNode = createNode(null, nodeFactory = nodeFactory, advertisedServices = ServiceInfo(NetworkMapService.type)) - val notaryNode = createNode(mapNode.info.address, nodeFactory = nodeFactory, overrideServices = notaryOverride, + val mapAddress = mapNode.network.myAddress + val notaryNode = createNode(mapAddress, nodeFactory = nodeFactory, overrideServices = notaryOverride, advertisedServices = notaryServiceInfo) val nodes = ArrayList() repeat(numPartyNodes) { - nodes += createPartyNode(mapNode.info.address) + nodes += createPartyNode(mapAddress) } nodes.forEach { itNode -> nodes.map { it.info.legalIdentityAndCert }.forEach(itNode.services.identityService::registerIdentity) diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt index 2126f18897..d7366d7663 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -1,5 +1,6 @@ package net.corda.testing.node +import com.google.common.net.HostAndPort import net.corda.core.contracts.Attachment import net.corda.core.crypto.* import net.corda.core.flows.StateMachineRunId @@ -73,7 +74,10 @@ open class MockServices(vararg val keys: KeyPair) : ServiceHub { override val vaultQueryService: VaultQueryService get() = throw UnsupportedOperationException() override val networkMapCache: NetworkMapCache get() = throw UnsupportedOperationException() override val clock: Clock get() = Clock.systemUTC() - override val myInfo: NodeInfo get() = NodeInfo(object : SingleMessageRecipient {}, getTestPartyAndCertificate(MEGA_CORP.name, key.public), MOCK_VERSION_INFO.platformVersion) + override val myInfo: NodeInfo get() { + val identity = getTestPartyAndCertificate(MEGA_CORP.name, key.public) + return NodeInfo(listOf(HostAndPort.fromHost("localhost")), identity, setOf(identity), MOCK_VERSION_INFO.platformVersion) + } override val transactionVerifierService: TransactionVerifierService get() = InMemoryTransactionVerifierService(2) fun makeVaultService(dataSourceProps: Properties, hibernateConfig: HibernateConfiguration = HibernateConfiguration(NodeSchemaService())): VaultService { diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt index d63c7ff366..0049c9e194 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt @@ -18,7 +18,7 @@ import net.corda.core.crypto.commonName import net.corda.core.div import net.corda.core.exists import net.corda.core.node.CityDatabase -import net.corda.core.node.PhysicalLocation +import net.corda.core.node.WorldMapLocation import net.corda.core.readAllLines import net.corda.core.utilities.normaliseLegalName import net.corda.core.utilities.validateLegalName @@ -211,7 +211,7 @@ class NodeTabView : Fragment() { CityDatabase.cityMap.values.map { it.countryCode }.toSet().map { it to Image(resources["/net/corda/demobench/flags/$it.png"]) }.toMap() } - private fun Pane.nearestCityField(): ComboBox { + private fun Pane.nearestCityField(): ComboBox { return combobox(model.nearestCity, CityDatabase.cityMap.values.toList().sortedBy { it.description }) { minWidth = textWidth styleClass += "city-picker" @@ -229,9 +229,9 @@ class NodeTabView : Fragment() { if (it == null) error("Please select a city") else null } - converter = object : StringConverter() { - override fun toString(loc: PhysicalLocation?) = loc?.description ?: "" - override fun fromString(string: String): PhysicalLocation? = CityDatabase[string] + converter = object : StringConverter() { + override fun toString(loc: WorldMapLocation?) = loc?.description ?: "" + override fun fromString(string: String): WorldMapLocation? = CityDatabase[string] } value = CityDatabase["London"] diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt index 6adfb1af73..44e08e45b2 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt @@ -98,7 +98,7 @@ class Network : CordaView() { copyableLabel(SimpleObjectProperty(node.legalIdentity.owningKey.toBase58String())).apply { minWidth = 400.0 } } row("Services :") { label(node.advertisedServices.map { it.info }.joinToString(", ")) } - node.physicalLocation?.apply { row("Location :") { label(this@apply.description) } } + node.worldMapLocation?.apply { row("Location :") { label(this@apply.description) } } } } setOnMouseClicked { @@ -122,7 +122,7 @@ class Network : CordaView() { contentDisplay = ContentDisplay.TOP val coordinate = Bindings.createObjectBinding({ // These coordinates are obtained when we generate the map using TileMill. - node.physicalLocation?.coordinate?.project(mapPane.width, mapPane.height, 85.0511, -85.0511, -180.0, 180.0) ?: Pair(0.0, 0.0) + node.worldMapLocation?.coordinate?.project(mapPane.width, mapPane.height, 85.0511, -85.0511, -180.0, 180.0) ?: Pair(0.0, 0.0) }, arrayOf(mapPane.widthProperty(), mapPane.heightProperty())) // Center point of the label. layoutXProperty().bind(coordinate.map { it.first - width / 2 }) diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/TransactionViewer.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/TransactionViewer.kt index d97292ac13..858117de3f 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/TransactionViewer.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/TransactionViewer.kt @@ -24,7 +24,6 @@ import net.corda.contracts.asset.Cash import net.corda.core.contracts.* import net.corda.core.crypto.* import net.corda.core.identity.AbstractParty -import net.corda.core.identity.AnonymousParty import net.corda.core.node.NodeInfo import net.corda.explorer.AmountDiff import net.corda.explorer.formatters.AmountFormatter diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt index e0883cc311..36ed583c7c 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt @@ -22,7 +22,6 @@ import net.corda.core.contracts.sumOrNull import net.corda.core.contracts.withoutIssuer import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party -import net.corda.core.crypto.commonName import net.corda.core.flows.FlowException import net.corda.core.getOrThrow import net.corda.core.messaging.startFlow From 77e1d54c436ea5da2b4908c796f65c639b8ba611 Mon Sep 17 00:00:00 2001 From: Andrzej Cichocki Date: Tue, 27 Jun 2017 19:06:14 +0100 Subject: [PATCH 09/97] Make OGSwapPricingCcpExample runnable from any working directory (#925) --- .../example/OGSwapPricingCcpExample.kt | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/analytics/example/OGSwapPricingCcpExample.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/analytics/example/OGSwapPricingCcpExample.kt index baffba45a0..3966488829 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/analytics/example/OGSwapPricingCcpExample.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/analytics/example/OGSwapPricingCcpExample.kt @@ -32,6 +32,10 @@ import com.opengamma.strata.product.common.BuySell import com.opengamma.strata.product.swap.type.FixedIborSwapConventions import com.opengamma.strata.report.ReportCalculationResults import com.opengamma.strata.report.trade.TradeReport +import net.corda.core.div +import net.corda.core.exists +import java.nio.file.Path +import java.nio.file.Paths import java.time.LocalDate /** @@ -59,31 +63,44 @@ class SwapPricingCcpExample { /** * The location of the data files. */ - private val PATH_CONFIG = "src/main/resources/" + private val resourcesUri = run { + // Find src/main/resources by walking up the directory tree starting at a classpath root: + var module = Paths.get(javaClass.getResource("/").toURI()) + val relative = "src" / "main" / "resources" + var path: Path + while (true) { + path = module.resolve(relative) + path.exists() && break + module = module.parent + } + path.toUri() + } + + private fun resourceLocator(uri: String) = ResourceLocator.ofUrl(resourcesUri.resolve(uri).toURL()) /** * The location of the curve calibration groups file for CCP1 and CCP2. */ - private val GROUPS_RESOURCE_CCP1 = ResourceLocator.of(ResourceLocator.FILE_URL_PREFIX + PATH_CONFIG + "example-calibration/curves/groups.csv") - private val GROUPS_RESOURCE_CCP2 = ResourceLocator.of(ResourceLocator.FILE_URL_PREFIX + PATH_CONFIG + "example-calibration/curves/groups-ccp2.csv") + private val GROUPS_RESOURCE_CCP1 = resourceLocator("example-calibration/curves/groups.csv") + private val GROUPS_RESOURCE_CCP2 = resourceLocator("example-calibration/curves/groups-ccp2.csv") /** * The location of the curve calibration settings file for CCP1 and CCP2. */ - private val SETTINGS_RESOURCE_CCP1 = ResourceLocator.of(ResourceLocator.FILE_URL_PREFIX + PATH_CONFIG + "example-calibration/curves/settings.csv") - private val SETTINGS_RESOURCE_CCP2 = ResourceLocator.of(ResourceLocator.FILE_URL_PREFIX + PATH_CONFIG + "example-calibration/curves/settings-ccp2.csv") + private val SETTINGS_RESOURCE_CCP1 = resourceLocator("example-calibration/curves/settings.csv") + private val SETTINGS_RESOURCE_CCP2 = resourceLocator("example-calibration/curves/settings-ccp2.csv") /** * The location of the curve calibration nodes file for CCP1 and CCP2. */ - private val CALIBRATION_RESOURCE_CCP1 = ResourceLocator.of(ResourceLocator.FILE_URL_PREFIX + PATH_CONFIG + "example-calibration/curves/calibrations.csv") - private val CALIBRATION_RESOURCE_CCP2 = ResourceLocator.of(ResourceLocator.FILE_URL_PREFIX + PATH_CONFIG + "example-calibration/curves/calibrations-ccp2.csv") + private val CALIBRATION_RESOURCE_CCP1 = resourceLocator("example-calibration/curves/calibrations.csv") + private val CALIBRATION_RESOURCE_CCP2 = resourceLocator("example-calibration/curves/calibrations-ccp2.csv") /** * The location of the market quotes file for CCP1 and CCP2. */ - private val QUOTES_RESOURCE_CCP1 = ResourceLocator.of(ResourceLocator.FILE_URL_PREFIX + PATH_CONFIG + "example-calibration/quotes/quotes.csv") - private val QUOTES_RESOURCE_CCP2 = ResourceLocator.of(ResourceLocator.FILE_URL_PREFIX + PATH_CONFIG + "example-calibration/quotes/quotes-ccp2.csv") + private val QUOTES_RESOURCE_CCP1 = resourceLocator("example-calibration/quotes/quotes.csv") + private val QUOTES_RESOURCE_CCP2 = resourceLocator("example-calibration/quotes/quotes-ccp2.csv") /** * The location of the historical fixing file. */ - private val FIXINGS_RESOURCE = ResourceLocator.of(ResourceLocator.FILE_URL_PREFIX + PATH_CONFIG + "example-marketdata/historical-fixings/usd-libor-3m.csv") + private val FIXINGS_RESOURCE = resourceLocator("example-marketdata/historical-fixings/usd-libor-3m.csv") /** * The first counterparty. From 0aadc037efce942b4723178d096b32e4e318117e Mon Sep 17 00:00:00 2001 From: Andrzej Cichocki Date: Wed, 28 Jun 2017 09:54:09 +0100 Subject: [PATCH 10/97] Make logging available in IntelliJ between gradle clean and assemble (#929) * Enforce absence of node from client rpc smokeTest classpath --- .idea/compiler.xml | 2 + build.gradle | 6 -- client/rpc/build.gradle | 5 +- .../corda/kotlin/rpc/ValidateClasspathTest.kt | 27 +++++++++ .../corda/core/utilities/ProcessUtilities.kt | 51 ----------------- .../kotlin/net/corda/node/BootTests.kt | 2 +- settings.gradle | 1 + smoke-test-utils/build.gradle | 1 + test-common/build.gradle | 1 + .../src/main/resources/log4j2-test.xml | 0 test-utils/build.gradle | 1 + .../kotlin/net/corda/testing/RPCDriver.kt | 2 +- .../kotlin/net/corda/testing/driver/Driver.kt | 4 +- .../corda/testing/driver/ProcessUtilities.kt | 57 +++++++++++++++++++ .../net/corda/verifier/VerifierDriver.kt | 2 +- 15 files changed, 96 insertions(+), 66 deletions(-) create mode 100644 client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/ValidateClasspathTest.kt delete mode 100644 core/src/main/kotlin/net/corda/core/utilities/ProcessUtilities.kt create mode 100644 test-common/build.gradle rename config/test/log4j2.xml => test-common/src/main/resources/log4j2-test.xml (100%) create mode 100644 test-utils/src/main/kotlin/net/corda/testing/driver/ProcessUtilities.kt diff --git a/.idea/compiler.xml b/.idea/compiler.xml index f89d9257e9..06d18a92b6 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -79,6 +79,8 @@ + + diff --git a/build.gradle b/build.gradle index 4515bb43da..6bc81509ac 100644 --- a/build.gradle +++ b/build.gradle @@ -103,12 +103,6 @@ allprojects { sourceCompatibility = 1.8 targetCompatibility = 1.8 - // Use manual resource copying of log4j2.xml rather than source sets. - // This prevents problems in IntelliJ with regard to duplicate source roots. - processTestResources { - from file("$rootDir/config/test/log4j2.xml") - } - tasks.withType(JavaCompile) { options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" << "-Xlint:-options" << "-parameters" } diff --git a/client/rpc/build.gradle b/client/rpc/build.gradle index b18b563b0f..7ff552073d 100644 --- a/client/rpc/build.gradle +++ b/client/rpc/build.gradle @@ -36,9 +36,6 @@ sourceSets { } processSmokeTestResources { - from(file("$rootDir/config/test/log4j2.xml")) { - rename 'log4j2\\.xml', 'log4j2-test.xml' - } from(project(':node:capsule').tasks.buildCordaJAR) { rename 'corda-(.*)', 'corda.jar' } @@ -85,4 +82,4 @@ jar { publish { name = jar.baseName -} \ No newline at end of file +} diff --git a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/ValidateClasspathTest.kt b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/ValidateClasspathTest.kt new file mode 100644 index 0000000000..ecc534ca8a --- /dev/null +++ b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/ValidateClasspathTest.kt @@ -0,0 +1,27 @@ +package net.corda.kotlin.rpc + +import net.corda.core.div +import org.junit.Test +import java.io.File +import java.nio.file.Path +import java.nio.file.Paths +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class ValidateClasspathTest { + @Test + fun `node not on classpath`() { + val paths = System.getProperty("java.class.path").split(File.pathSeparatorChar).map { Paths.get(it) } + // First find core so that if node is there, it's in the form we expect: + assertFalse(paths.filter { it.contains("core" / "build") }.isEmpty()) + assertTrue(paths.filter { it.contains("node" / "build") }.isEmpty()) + } +} + +private fun Path.contains(that: Path): Boolean { + val size = that.nameCount + (0..nameCount - size).forEach { + if (subpath(it, it + size) == that) return true + } + return false +} diff --git a/core/src/main/kotlin/net/corda/core/utilities/ProcessUtilities.kt b/core/src/main/kotlin/net/corda/core/utilities/ProcessUtilities.kt deleted file mode 100644 index d69ab38367..0000000000 --- a/core/src/main/kotlin/net/corda/core/utilities/ProcessUtilities.kt +++ /dev/null @@ -1,51 +0,0 @@ -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, classpath, jdwpPort, extraJvmArguments, inheritIO, errorLogPath, workingDirectory) - } - - fun startJavaProcess( - className: String, - arguments: List, - classpath: String = defaultClassPath, - jdwpPort: Int? = null, - extraJvmArguments: List = emptyList(), - inheritIO: Boolean = true, - errorLogPath: Path? = null, - workingDirectory: Path? = null - ): Process { - val separator = System.getProperty("file.separator") - 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") - } else { - emptyList() - } - - val allArguments = listOf(javaPath) + - debugPortArgument + - listOf("-Xmx200m", "-XX:+UseG1GC") + - extraJvmArguments + - listOf("-cp", classpath, className) + - arguments.toList() - return ProcessBuilder(allArguments).apply { - if (errorLogPath != null) redirectError(errorLogPath.toFile()) - if (inheritIO) inheritIO() - if (workingDirectory != null) directory(workingDirectory.toFile()) - }.start() - } - - val defaultClassPath: String get() = System.getProperty("java.class.path") -} diff --git a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt index 1bae088182..f356852b09 100644 --- a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt @@ -37,7 +37,7 @@ class BootTests { assertThat(logConfigFile).isRegularFile() driver(isDebug = true, systemProperties = mapOf("log4j.configurationFile" to logConfigFile.toString())) { val alice = startNode(ALICE.name).get() - val logFolder = alice.configuration.baseDirectory / "logs" + val logFolder = alice.configuration.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME val logFile = logFolder.toFile().listFiles { _, name -> name.endsWith(".log") }.single() // Start second Alice, should fail assertThatThrownBy { diff --git a/settings.gradle b/settings.gradle index 1e35747a78..edfef044a8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -19,6 +19,7 @@ include 'experimental' include 'experimental:sandbox' include 'experimental:quasar-hook' include 'verifier' +include 'test-common' include 'test-utils' include 'smoke-test-utils' include 'tools:explorer' diff --git a/smoke-test-utils/build.gradle b/smoke-test-utils/build.gradle index dcd94fae66..0868951903 100644 --- a/smoke-test-utils/build.gradle +++ b/smoke-test-utils/build.gradle @@ -4,5 +4,6 @@ description 'Utilities needed for smoke tests in Corda' dependencies { // Smoke tests do NOT have any Node code on the classpath! + compile project(':test-common') compile project(':client:rpc') } diff --git a/test-common/build.gradle b/test-common/build.gradle new file mode 100644 index 0000000000..472fa597c3 --- /dev/null +++ b/test-common/build.gradle @@ -0,0 +1 @@ +// Nothing needed here currently. diff --git a/config/test/log4j2.xml b/test-common/src/main/resources/log4j2-test.xml similarity index 100% rename from config/test/log4j2.xml rename to test-common/src/main/resources/log4j2-test.xml diff --git a/test-utils/build.gradle b/test-utils/build.gradle index 3c5e40f6fe..6b0dc9b873 100644 --- a/test-utils/build.gradle +++ b/test-utils/build.gradle @@ -27,6 +27,7 @@ sourceSets { } dependencies { + compile project(':test-common') compile project(':finance') compile project(':core') compile project(':node') diff --git a/test-utils/src/main/kotlin/net/corda/testing/RPCDriver.kt b/test-utils/src/main/kotlin/net/corda/testing/RPCDriver.kt index 326d01fb74..f4d09c8d23 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/RPCDriver.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/RPCDriver.kt @@ -12,7 +12,7 @@ import net.corda.core.div import net.corda.core.map import net.corda.core.messaging.RPCOps import net.corda.core.random63BitValue -import net.corda.core.utilities.ProcessUtilities +import net.corda.testing.driver.ProcessUtilities import net.corda.node.services.RPCUserService import net.corda.node.services.messaging.ArtemisMessagingServer import net.corda.node.services.messaging.RPCServer diff --git a/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt b/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt index 5e77d52cbb..4667a52bef 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -792,7 +792,7 @@ class DriverDSL( "-javaagent:$quasarJarPath" val loggingLevel = if (debugPort == null) "INFO" else "DEBUG" - ProcessUtilities.startJavaProcess( + ProcessUtilities.startCordaProcess( className = "net.corda.node.Corda", // cannot directly get class for this, so just use string arguments = listOf( "--base-directory=${nodeConf.baseDirectory}", @@ -817,7 +817,7 @@ class DriverDSL( ): ListenableFuture { return executorService.submit { val className = "net.corda.webserver.WebServer" - ProcessUtilities.startJavaProcess( + ProcessUtilities.startCordaProcess( className = className, // cannot directly get class for this, so just use string arguments = listOf("--base-directory", handle.configuration.baseDirectory.toString()), jdwpPort = debugPort, diff --git a/test-utils/src/main/kotlin/net/corda/testing/driver/ProcessUtilities.kt b/test-utils/src/main/kotlin/net/corda/testing/driver/ProcessUtilities.kt new file mode 100644 index 0000000000..7ac9eedf94 --- /dev/null +++ b/test-utils/src/main/kotlin/net/corda/testing/driver/ProcessUtilities.kt @@ -0,0 +1,57 @@ +package net.corda.testing.driver + +import net.corda.core.div +import net.corda.core.exists +import java.io.File.pathSeparator +import java.nio.file.Path + +object ProcessUtilities { + inline fun startJavaProcess( + arguments: List, + jdwpPort: Int? = null + ): Process { + return startJavaProcessImpl(C::class.java.name, arguments, defaultClassPath, jdwpPort, emptyList(), null, null) + } + + fun startCordaProcess( + className: String, + arguments: List, + jdwpPort: Int?, + extraJvmArguments: List, + errorLogPath: Path?, + workingDirectory: Path? = null + ): Process { + // FIXME: Instead of hacking our classpath, use the correct classpath for className. + val classpath = defaultClassPath.split(pathSeparator).filter { !(it / "log4j2-test.xml").exists() }.joinToString(pathSeparator) + return startJavaProcessImpl(className, arguments, classpath, jdwpPort, extraJvmArguments, errorLogPath, workingDirectory) + } + + fun startJavaProcessImpl( + className: String, + arguments: List, + classpath: String, + jdwpPort: Int?, + extraJvmArguments: List, + errorLogPath: Path?, + workingDirectory: Path? + ): Process { + val command = mutableListOf().apply { + add((System.getProperty("java.home") / "bin" / "java").toString()) + (jdwpPort != null) && add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$jdwpPort") + add("-Xmx200m") + add("-XX:+UseG1GC") + addAll(extraJvmArguments) + add("-cp") + add(classpath) + add(className) + addAll(arguments) + } + return ProcessBuilder(command).apply { + if (errorLogPath != null) redirectError(errorLogPath.toFile()) // FIXME: Undone by inheritIO. + inheritIO() + if (workingDirectory != null) directory(workingDirectory.toFile()) + }.start() + } + + val defaultClassPath: String get() = System.getProperty("java.class.path") +} diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt index a343c3bd2c..13cdb8d6aa 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt @@ -13,7 +13,7 @@ import net.corda.core.div import net.corda.core.map import net.corda.core.random63BitValue import net.corda.core.transactions.LedgerTransaction -import net.corda.core.utilities.ProcessUtilities +import net.corda.testing.driver.ProcessUtilities import net.corda.core.utilities.loggerFor import net.corda.node.services.config.configureDevKeyAndTrustStores import net.corda.nodeapi.ArtemisMessagingComponent.Companion.NODE_USER From e02c37c06da65368adaf2cdda554460351f1576a Mon Sep 17 00:00:00 2001 From: Patrick Kuo Date: Wed, 28 Jun 2017 11:06:06 +0100 Subject: [PATCH 11/97] Replace kotlin Pair with `DataFeed` data class (#930) * Replace kotlin Pair with DataFeed data class * remove unintended changes * minor fix * address PR issues --- .../kotlin/net/corda/core/flows/FlowLogic.kt | 12 ++--- .../net/corda/core/messaging/CordaRPCOps.kt | 47 ++++++++++++++----- .../core/node/services/NetworkMapCache.kt | 3 +- .../net/corda/core/node/services/Services.kt | 13 ++--- ...achineRecordedTransactionMappingStorage.kt | 3 +- .../core/node/services/TransactionStorage.kt | 3 +- .../corda/node/internal/CordaRPCOpsImpl.kt | 15 +++--- .../network/InMemoryNetworkMapCache.kt | 5 +- .../DBTransactionMappingStorage.kt | 5 +- .../persistence/DBTransactionStorage.kt | 5 +- ...achineRecordedTransactionMappingStorage.kt | 5 +- .../statemachine/StateMachineManager.kt | 15 ++++-- .../node/services/vault/NodeVaultService.kt | 5 +- .../node/messaging/TwoPartyTradeFlowTests.kt | 3 +- .../net/corda/testing/node/MockServices.kt | 5 +- 15 files changed, 90 insertions(+), 54 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt index a73cda8427..4583d14a62 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt @@ -4,13 +4,13 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.core.crypto.SecureHash import net.corda.core.identity.Party import net.corda.core.internal.FlowStateMachine +import net.corda.core.messaging.DataFeed import net.corda.core.node.ServiceHub import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.UntrustworthyData import net.corda.core.utilities.debug import org.slf4j.Logger -import rx.Observable /** * A sub-class of [FlowLogic] implements a flow using direct, straight line blocking code. Thus you @@ -180,7 +180,7 @@ abstract class FlowLogic { * @param extraAuditData in the audit log for this permission check these extra key value pairs will be recorded. */ @Throws(FlowException::class) - fun checkFlowPermission(permissionName: String, extraAuditData: Map) = stateMachine.checkFlowPermission(permissionName, extraAuditData) + fun checkFlowPermission(permissionName: String, extraAuditData: Map) = stateMachine.checkFlowPermission(permissionName, extraAuditData) /** @@ -189,7 +189,7 @@ abstract class FlowLogic { * @param comment a general human readable summary of the event. * @param extraAuditData in the audit log for this permission check these extra key value pairs will be recorded. */ - fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map) = stateMachine.recordAuditEvent(eventType, comment, extraAuditData) + fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map) = stateMachine.recordAuditEvent(eventType, comment, extraAuditData) /** * Override this to provide a [ProgressTracker]. If one is provided and stepped, the framework will do something @@ -215,10 +215,10 @@ abstract class FlowLogic { * * @return Returns null if this flow has no progress tracker. */ - fun track(): Pair>? { + fun track(): DataFeed? { // TODO this is not threadsafe, needs an atomic get-step-and-subscribe return progressTracker?.let { - it.currentStep.label to it.changes.map { it.toString() } + DataFeed(it.currentStep.label, it.changes.map { it.toString() }) } } @@ -230,7 +230,7 @@ abstract class FlowLogic { @Suspendable fun waitForLedgerCommit(hash: SecureHash): SignedTransaction = stateMachine.waitForLedgerCommit(hash, this) - //////////////////////////////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////////////////////////////// private var _stateMachine: FlowStateMachine<*>? = null /** diff --git a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt index bfe9910de8..807519cb96 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt @@ -32,7 +32,7 @@ data class StateMachineInfo( val id: StateMachineRunId, val flowLogicClassName: String, val initiator: FlowInitiator, - val progressTrackerStepAndUpdates: Pair>? + val progressTrackerStepAndUpdates: DataFeed? ) { override fun toString(): String = "${javaClass.simpleName}($id, $flowLogicClassName)" } @@ -52,9 +52,6 @@ sealed class StateMachineUpdate { * RPC operations that the node exposes to clients using the Java client library. These can be called from * client apps and are implemented by the node in the [net.corda.node.internal.CordaRPCOpsImpl] class. */ - -// TODO: The use of Pairs throughout is unfriendly for Java interop. - interface CordaRPCOps : RPCOps { /** * Returns the RPC protocol version, which is the same the node's Platform Version. Exists since version 1 so guaranteed @@ -63,10 +60,13 @@ interface CordaRPCOps : RPCOps { override val protocolVersion: Int get() = nodeIdentity().platformVersion /** - * Returns a pair of currently in-progress state machine infos and an observable of future state machine adds/removes. + * Returns a data feed of currently in-progress state machine infos and an observable of future state machine adds/removes. */ @RPCReturnsObservables - fun stateMachinesAndUpdates(): Pair, Observable> + fun stateMachinesFeed(): DataFeed, StateMachineUpdate> + + @Deprecated("This function will be removed in a future milestone", ReplaceWith("stateMachinesFeed()")) + fun stateMachinesAndUpdates() = stateMachinesFeed() /** * Returns a snapshot of vault states for a given query criteria (and optional order and paging specification) @@ -119,7 +119,7 @@ interface CordaRPCOps : RPCOps { * * Notes: the snapshot part of the query adheres to the same behaviour as the [queryBy] function. * the [QueryCriteria] applies to both snapshot and deltas (streaming updates). - */ + */ // DOCSTART VaultTrackByAPI @RPCReturnsObservables fun vaultTrackBy(criteria: QueryCriteria, @@ -147,31 +147,41 @@ interface CordaRPCOps : RPCOps { // DOCEND VaultTrackAPIHelpers /** - * Returns a pair of head states in the vault and an observable of future updates to the vault. + * Returns a data feed of head states in the vault and an observable of future updates to the vault. */ @RPCReturnsObservables // TODO: Remove this from the interface @Deprecated("This function will be removed in a future milestone", ReplaceWith("vaultTrackBy(QueryCriteria())")) - fun vaultAndUpdates(): Pair>, Observable> + fun vaultAndUpdates(): DataFeed>, Vault.Update> /** - * Returns a pair of all recorded transactions and an observable of future recorded ones. + * Returns a data feed of all recorded transactions and an observable of future recorded ones. */ @RPCReturnsObservables - fun verifiedTransactions(): Pair, Observable> + fun verifiedTransactionsFeed(): DataFeed, SignedTransaction> + + @Deprecated("This function will be removed in a future milestone", ReplaceWith("verifiedTransactionFeed()")) + fun verifiedTransactions() = verifiedTransactionsFeed() + /** * Returns a snapshot list of existing state machine id - recorded transaction hash mappings, and a stream of future * such mappings as well. */ @RPCReturnsObservables - fun stateMachineRecordedTransactionMapping(): Pair, Observable> + fun stateMachineRecordedTransactionMappingFeed(): DataFeed, StateMachineTransactionMapping> + + @Deprecated("This function will be removed in a future milestone", ReplaceWith("stateMachineRecordedTransactionMappingFeed()")) + fun stateMachineRecordedTransactionMapping() = stateMachineRecordedTransactionMappingFeed() /** * Returns all parties currently visible on the network with their advertised services and an observable of future updates to the network. */ @RPCReturnsObservables - fun networkMapUpdates(): Pair, Observable> + fun networkMapFeed(): DataFeed, NetworkMapCache.MapChange> + + @Deprecated("This function will be removed in a future milestone", ReplaceWith("networkMapFeed()")) + fun networkMapUpdates() = networkMapFeed() /** * Start the given flow with the given arguments. [logicType] must be annotated with [net.corda.core.flows.StartableByRPC]. @@ -382,3 +392,14 @@ inline fun > CordaRPCOps.startTrac arg2: C, arg3: D ): FlowProgressHandle = startTrackedFlowDynamic(R::class.java, arg0, arg1, arg2, arg3) + +/** + * The Data feed contains a snapshot of the requested data and an [Observable] of future updates. + */ +@CordaSerializable +data class DataFeed(val snapshot: A, val updates: Observable) { + @Deprecated("This function will be removed in a future milestone", ReplaceWith("snapshot")) + val first: A get() = snapshot + @Deprecated("This function will be removed in a future milestone", ReplaceWith("updates")) + val second: Observable get() = updates +} diff --git a/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt b/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt index 027383031d..7713bc35bb 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt @@ -3,6 +3,7 @@ package net.corda.core.node.services import com.google.common.util.concurrent.ListenableFuture import net.corda.core.contracts.Contract import net.corda.core.identity.Party +import net.corda.core.messaging.DataFeed import net.corda.core.node.NodeInfo import net.corda.core.randomOrNull import net.corda.core.serialization.CordaSerializable @@ -48,7 +49,7 @@ interface NetworkMapCache { * Atomically get the current party nodes and a stream of updates. Note that the Observable buffers updates until the * first subscriber is registered so as to avoid racing with early updates. */ - fun track(): Pair, Observable> + fun track(): DataFeed, MapChange> /** Get the collection of nodes which advertise a specific service. */ fun getNodesWithService(serviceType: ServiceType): List { diff --git a/core/src/main/kotlin/net/corda/core/node/services/Services.kt b/core/src/main/kotlin/net/corda/core/node/services/Services.kt index dfb7c4c5e1..db14d69bfd 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/Services.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/Services.kt @@ -3,7 +3,6 @@ package net.corda.core.node.services import co.paralleluniverse.fibers.Suspendable import com.google.common.util.concurrent.ListenableFuture import net.corda.core.contracts.* -import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.SecureHash @@ -12,7 +11,9 @@ import net.corda.core.flows.FlowException import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate +import net.corda.core.messaging.DataFeed import net.corda.core.node.services.vault.PageSpecification +import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.Sort import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.OpaqueBytes @@ -71,9 +72,9 @@ class Vault(val states: Iterable>) { /** Checks whether the update contains a state of the specified type and state status */ fun containsType(clazz: Class, status: StateStatus) = - when(status) { + when (status) { StateStatus.UNCONSUMED -> produced.any { clazz.isAssignableFrom(it.state.data.javaClass) } - StateStatus.CONSUMED -> consumed.any { clazz.isAssignableFrom(it.state.data.javaClass) } + StateStatus.CONSUMED -> consumed.any { clazz.isAssignableFrom(it.state.data.javaClass) } else -> consumed.any { clazz.isAssignableFrom(it.state.data.javaClass) } || produced.any { clazz.isAssignableFrom(it.state.data.javaClass) } } @@ -142,7 +143,7 @@ class Vault(val states: Iterable>) { val lockUpdateTime: Instant?) @CordaSerializable - data class PageAndUpdates (val current: Vault.Page, val future: Observable) + data class PageAndUpdates(val current: Vault.Page, val future: Observable) } /** @@ -189,7 +190,7 @@ interface VaultService { */ // TODO: Remove this from the interface @Deprecated("This function will be removed in a future milestone", ReplaceWith("trackBy(QueryCriteria())")) - fun track(): Pair, Observable> + fun track(): DataFeed, Vault.Update> /** * Return unconsumed [ContractState]s for a given set of [StateRef]s @@ -274,7 +275,7 @@ interface VaultService { * Optionally may specify whether to include [StateRef] that have been marked as soft locked (default is true) */ // TODO: Remove this from the interface - @Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(QueryCriteria())")) + @Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(QueryCriteria())")) fun states(clazzes: Set>, statuses: EnumSet, includeSoftLockedStates: Boolean = true): Iterable> // DOCEND VaultStatesQuery diff --git a/core/src/main/kotlin/net/corda/core/node/services/StateMachineRecordedTransactionMappingStorage.kt b/core/src/main/kotlin/net/corda/core/node/services/StateMachineRecordedTransactionMappingStorage.kt index 66171bd2ed..98ca2272c2 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/StateMachineRecordedTransactionMappingStorage.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/StateMachineRecordedTransactionMappingStorage.kt @@ -2,6 +2,7 @@ package net.corda.core.node.services import net.corda.core.crypto.SecureHash import net.corda.core.flows.StateMachineRunId +import net.corda.core.messaging.DataFeed import net.corda.core.serialization.CordaSerializable import rx.Observable @@ -14,5 +15,5 @@ data class StateMachineTransactionMapping(val stateMachineRunId: StateMachineRun */ interface StateMachineRecordedTransactionMappingStorage { fun addMapping(stateMachineRunId: StateMachineRunId, transactionId: SecureHash) - fun track(): Pair, Observable> + fun track(): DataFeed, StateMachineTransactionMapping> } diff --git a/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt b/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt index a6788cc1d2..380ba12365 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt @@ -1,6 +1,7 @@ package net.corda.core.node.services import net.corda.core.crypto.SecureHash +import net.corda.core.messaging.DataFeed import net.corda.core.transactions.SignedTransaction import rx.Observable @@ -22,7 +23,7 @@ interface ReadOnlyTransactionStorage { /** * Returns all currently stored transactions and further fresh ones. */ - fun track(): Pair, Observable> + fun track(): DataFeed, SignedTransaction> } /** diff --git a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt index 479b58c8db..881ae4644d 100644 --- a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt @@ -14,7 +14,6 @@ import net.corda.core.node.NodeInfo import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.StateMachineTransactionMapping import net.corda.core.node.services.Vault -import net.corda.core.node.services.queryBy import net.corda.core.node.services.vault.PageSpecification import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.Sort @@ -43,16 +42,16 @@ class CordaRPCOpsImpl( private val smm: StateMachineManager, private val database: Database ) : CordaRPCOps { - override fun networkMapUpdates(): Pair, Observable> { + override fun networkMapFeed(): DataFeed, NetworkMapCache.MapChange> { return database.transaction { services.networkMapCache.track() } } - override fun vaultAndUpdates(): Pair>, Observable> { + override fun vaultAndUpdates(): DataFeed>, Vault.Update> { return database.transaction { val (vault, updates) = services.vaultService.track() - Pair(vault.states.toList(), updates) + DataFeed(vault.states.toList(), updates) } } @@ -75,23 +74,23 @@ class CordaRPCOpsImpl( } } - override fun verifiedTransactions(): Pair, Observable> { + override fun verifiedTransactionsFeed(): DataFeed, SignedTransaction> { return database.transaction { services.storageService.validatedTransactions.track() } } - override fun stateMachinesAndUpdates(): Pair, Observable> { + override fun stateMachinesFeed(): DataFeed, StateMachineUpdate> { return database.transaction { val (allStateMachines, changes) = smm.track() - Pair( + DataFeed( allStateMachines.map { stateMachineInfoFromFlowLogic(it.logic) }, changes.map { stateMachineUpdateFromStateMachineChange(it) } ) } } - override fun stateMachineRecordedTransactionMapping(): Pair, Observable> { + override fun stateMachineRecordedTransactionMappingFeed(): DataFeed, StateMachineTransactionMapping> { return database.transaction { services.storageService.stateMachineRecordedTransactionMapping.track() } diff --git a/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt b/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt index f25c210c59..ab5bbeee03 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt @@ -6,6 +6,7 @@ import com.google.common.util.concurrent.SettableFuture import net.corda.core.bufferUntilSubscribed import net.corda.core.identity.Party import net.corda.core.map +import net.corda.core.messaging.DataFeed import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.NodeInfo import net.corda.core.node.services.DEFAULT_SESSION_ID @@ -71,9 +72,9 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach override fun getNodeByLegalIdentityKey(identityKey: PublicKey): NodeInfo? = registeredNodes[identityKey] - override fun track(): Pair, Observable> { + override fun track(): DataFeed, MapChange> { synchronized(_changed) { - return Pair(partyNodes, _changed.bufferUntilSubscribed().wrapWithDatabaseTransaction()) + return DataFeed(partyNodes, _changed.bufferUntilSubscribed().wrapWithDatabaseTransaction()) } } diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionMappingStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionMappingStorage.kt index 69cdf43e5c..bbf58c3524 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionMappingStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionMappingStorage.kt @@ -4,6 +4,7 @@ import net.corda.core.ThreadBox import net.corda.core.bufferUntilSubscribed import net.corda.core.crypto.SecureHash import net.corda.core.flows.StateMachineRunId +import net.corda.core.messaging.DataFeed import net.corda.core.node.services.StateMachineRecordedTransactionMappingStorage import net.corda.core.node.services.StateMachineTransactionMapping import net.corda.node.utilities.* @@ -55,9 +56,9 @@ class DBTransactionMappingStorage : StateMachineRecordedTransactionMappingStorag } } - override fun track(): Pair, Observable> { + override fun track(): DataFeed, StateMachineTransactionMapping> { mutex.locked { - return Pair( + return DataFeed( stateMachineTransactionMap.map { StateMachineTransactionMapping(it.value, it.key) }, updates.bufferUntilSubscribed().wrapWithDatabaseTransaction() ) diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt index 04e42dcb13..51139177f1 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt @@ -3,6 +3,7 @@ package net.corda.node.services.persistence import com.google.common.annotations.VisibleForTesting import net.corda.core.bufferUntilSubscribed import net.corda.core.crypto.SecureHash +import net.corda.core.messaging.DataFeed import net.corda.core.node.services.TransactionStorage import net.corda.core.transactions.SignedTransaction import net.corda.node.utilities.* @@ -61,9 +62,9 @@ class DBTransactionStorage : TransactionStorage { val updatesPublisher = PublishSubject.create().toSerialized() override val updates: Observable = updatesPublisher.wrapWithDatabaseTransaction() - override fun track(): Pair, Observable> { + override fun track(): DataFeed, SignedTransaction> { synchronized(txStorage) { - return Pair(txStorage.values.toList(), updatesPublisher.bufferUntilSubscribed().wrapWithDatabaseTransaction()) + return DataFeed(txStorage.values.toList(), updatesPublisher.bufferUntilSubscribed().wrapWithDatabaseTransaction()) } } diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/InMemoryStateMachineRecordedTransactionMappingStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/InMemoryStateMachineRecordedTransactionMappingStorage.kt index 301fde426f..f0aaa50e86 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/InMemoryStateMachineRecordedTransactionMappingStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/InMemoryStateMachineRecordedTransactionMappingStorage.kt @@ -4,6 +4,7 @@ import net.corda.core.ThreadBox import net.corda.core.bufferUntilSubscribed import net.corda.core.crypto.SecureHash import net.corda.core.flows.StateMachineRunId +import net.corda.core.messaging.DataFeed import net.corda.core.node.services.StateMachineRecordedTransactionMappingStorage import net.corda.core.node.services.StateMachineTransactionMapping import rx.Observable @@ -32,9 +33,9 @@ class InMemoryStateMachineRecordedTransactionMappingStorage : StateMachineRecord } override fun track(): - Pair, Observable> { + DataFeed, StateMachineTransactionMapping> { mutex.locked { - return Pair( + return DataFeed( stateMachineTransactionMap.flatMap { entry -> entry.value.map { StateMachineTransactionMapping(entry.key, it) diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt index 595ac8dc47..eac032e0e3 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt @@ -16,8 +16,12 @@ import com.google.common.util.concurrent.ListenableFuture import io.requery.util.CloseableIterator import net.corda.core.* import net.corda.core.crypto.SecureHash -import net.corda.core.flows.* +import net.corda.core.flows.FlowException +import net.corda.core.flows.FlowInitiator +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.StateMachineRunId import net.corda.core.identity.Party +import net.corda.core.messaging.DataFeed import net.corda.core.serialization.* import net.corda.core.utilities.debug import net.corda.core.utilities.loggerFor @@ -88,6 +92,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, } throw UnsupportedOperationException(message) } + override fun read(kryo: Kryo, input: Input, type: Class) = throw IllegalStateException("Should not reach here!") } @@ -107,8 +112,8 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, sealed class Change { abstract val logic: FlowLogic<*> - data class Add(override val logic: FlowLogic<*>): Change() - data class Removed(override val logic: FlowLogic<*>, val result: ErrorOr<*>): Change() + data class Add(override val logic: FlowLogic<*>) : Change() + data class Removed(override val logic: FlowLogic<*>, val result: ErrorOr<*>) : Change() } // A list of all the state machines being managed by this class. We expose snapshots of it via the stateMachines @@ -226,9 +231,9 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, * Atomic get snapshot + subscribe. This is needed so we don't miss updates between subscriptions to [changes] and * calls to [allStateMachines] */ - fun track(): Pair>, Observable> { + fun track(): DataFeed>, Change> { return mutex.locked { - Pair(stateMachines.keys.toList(), changesPublisher.bufferUntilSubscribed().wrapWithDatabaseTransaction()) + DataFeed(stateMachines.keys.toList(), changesPublisher.bufferUntilSubscribed().wrapWithDatabaseTransaction()) } } diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index 0f014302c1..159f784bdb 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -19,6 +19,7 @@ import net.corda.core.crypto.containsAny import net.corda.core.crypto.toBase58String import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party +import net.corda.core.messaging.DataFeed import net.corda.core.node.ServiceHub import net.corda.core.node.services.* import net.corda.core.serialization.* @@ -170,9 +171,9 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P override val updatesPublisher: PublishSubject get() = mutex.locked { _updatesPublisher } - override fun track(): Pair, Observable> { + override fun track(): DataFeed, Vault.Update> { return mutex.locked { - Pair(Vault(unconsumedStates()), _updatesPublisher.bufferUntilSubscribed().wrapWithDatabaseTransaction()) + DataFeed(Vault(unconsumedStates()), _updatesPublisher.bufferUntilSubscribed().wrapWithDatabaseTransaction()) } } diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index 493fe77ce1..c8d8876680 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -14,6 +14,7 @@ import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.internal.FlowStateMachine +import net.corda.core.messaging.DataFeed import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.NodeInfo import net.corda.core.node.services.* @@ -670,7 +671,7 @@ class TwoPartyTradeFlowTests { class RecordingTransactionStorage(val database: Database, val delegate: TransactionStorage) : TransactionStorage { - override fun track(): Pair, Observable> { + override fun track(): DataFeed, SignedTransaction> { return database.transaction { delegate.track() } diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt index d7366d7663..c3dd7e6e1f 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -5,6 +5,7 @@ import net.corda.core.contracts.Attachment import net.corda.core.crypto.* import net.corda.core.flows.StateMachineRunId import net.corda.core.identity.PartyAndCertificate +import net.corda.core.messaging.DataFeed import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub @@ -157,8 +158,8 @@ class MockStateMachineRecordedTransactionMappingStorage( ) : StateMachineRecordedTransactionMappingStorage by storage open class MockTransactionStorage : TransactionStorage { - override fun track(): Pair, Observable> { - return Pair(txns.values.toList(), _updatesPublisher) + override fun track(): DataFeed, SignedTransaction> { + return DataFeed(txns.values.toList(), _updatesPublisher) } private val txns = HashMap() From c3ca2744aab4883a18c4bc678a450b8423c4529f Mon Sep 17 00:00:00 2001 From: Patrick Kuo Date: Wed, 28 Jun 2017 11:29:14 +0100 Subject: [PATCH 12/97] Replace Vault.PageAndUpdates with DataFeed data class (#931) * Replace kotlin Pair with DataFeed data class * remove unintended changes * Replace Vault.PageAndUpdates with DataFeed data class * Remove PageAndUpdates --- .../net/corda/core/messaging/CordaRPCOps.kt | 16 ++++++++----- .../net/corda/core/node/services/Services.kt | 23 ++++++++----------- .../corda/node/internal/CordaRPCOpsImpl.kt | 4 ++-- .../services/vault/HibernateVaultQueryImpl.kt | 17 +++++++------- .../services/vault/VaultQueryJavaTests.java | 5 ++-- 5 files changed, 33 insertions(+), 32 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt index 807519cb96..cc79f7e26a 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt @@ -125,23 +125,23 @@ interface CordaRPCOps : RPCOps { fun vaultTrackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, - contractType: Class): Vault.PageAndUpdates + contractType: Class): DataFeed, Vault.Update> // DOCEND VaultTrackByAPI // Note: cannot apply @JvmOverloads to interfaces nor interface implementations // Java Helpers // DOCSTART VaultTrackAPIHelpers - fun vaultTrack(contractType: Class): Vault.PageAndUpdates { + fun vaultTrack(contractType: Class): DataFeed, Vault.Update> { return vaultTrackBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractType) } - fun vaultTrackByCriteria(contractType: Class, criteria: QueryCriteria): Vault.PageAndUpdates { + fun vaultTrackByCriteria(contractType: Class, criteria: QueryCriteria): DataFeed, Vault.Update> { return vaultTrackBy(criteria, PageSpecification(), Sort(emptySet()), contractType) } - fun vaultTrackByWithPagingSpec(contractType: Class, criteria: QueryCriteria, paging: PageSpecification): Vault.PageAndUpdates { + fun vaultTrackByWithPagingSpec(contractType: Class, criteria: QueryCriteria, paging: PageSpecification): DataFeed, Vault.Update> { return vaultTrackBy(criteria, paging, Sort(emptySet()), contractType) } - fun vaultTrackByWithSorting(contractType: Class, criteria: QueryCriteria, sorting: Sort): Vault.PageAndUpdates { + fun vaultTrackByWithSorting(contractType: Class, criteria: QueryCriteria, sorting: Sort): DataFeed, Vault.Update> { return vaultTrackBy(criteria, PageSpecification(), sorting, contractType) } // DOCEND VaultTrackAPIHelpers @@ -302,7 +302,7 @@ inline fun CordaRPCOps.vaultQueryBy(criteria: QueryC inline fun CordaRPCOps.vaultTrackBy(criteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(), paging: PageSpecification = PageSpecification(), - sorting: Sort = Sort(emptySet())): Vault.PageAndUpdates { + sorting: Sort = Sort(emptySet())): DataFeed, Vault.Update> { return vaultTrackBy(criteria, paging, sorting, T::class.java) } @@ -402,4 +402,8 @@ data class DataFeed(val snapshot: A, val updates: Observable) { val first: A get() = snapshot @Deprecated("This function will be removed in a future milestone", ReplaceWith("updates")) val second: Observable get() = updates + @Deprecated("This function will be removed in a future milestone", ReplaceWith("snapshot")) + val current: A get() = snapshot + @Deprecated("This function will be removed in a future milestone", ReplaceWith("updates")) + val future: Observable get() = updates } diff --git a/core/src/main/kotlin/net/corda/core/node/services/Services.kt b/core/src/main/kotlin/net/corda/core/node/services/Services.kt index db14d69bfd..432140cf8f 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/Services.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/Services.kt @@ -141,9 +141,6 @@ class Vault(val states: Iterable>) { val notaryKey: String, val lockId: String?, val lockUpdateTime: Instant?) - - @CordaSerializable - data class PageAndUpdates(val current: Vault.Page, val future: Observable) } /** @@ -382,7 +379,7 @@ interface VaultQueryService { fun _trackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, - contractType: Class): Vault.PageAndUpdates + contractType: Class): DataFeed, Vault.Update> // DOCEND VaultQueryAPI // Note: cannot apply @JvmOverloads to interfaces nor interface implementations @@ -406,16 +403,16 @@ interface VaultQueryService { fun trackBy(contractType: Class): Vault.Page { return _queryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractType) } - fun trackBy(contractType: Class, criteria: QueryCriteria): Vault.PageAndUpdates { + fun trackBy(contractType: Class, criteria: QueryCriteria): DataFeed, Vault.Update> { return _trackBy(criteria, PageSpecification(), Sort(emptySet()), contractType) } - fun trackBy(contractType: Class, criteria: QueryCriteria, paging: PageSpecification): Vault.PageAndUpdates { + fun trackBy(contractType: Class, criteria: QueryCriteria, paging: PageSpecification): DataFeed, Vault.Update> { return _trackBy(criteria, paging, Sort(emptySet()), contractType) } - fun trackBy(contractType: Class, criteria: QueryCriteria, sorting: Sort): Vault.PageAndUpdates { + fun trackBy(contractType: Class, criteria: QueryCriteria, sorting: Sort): DataFeed, Vault.Update> { return _trackBy(criteria, PageSpecification(), sorting, contractType) } - fun trackBy(contractType: Class, criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): Vault.PageAndUpdates { + fun trackBy(contractType: Class, criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): DataFeed, Vault.Update> { return _trackBy(criteria, paging, sorting, contractType) } } @@ -440,23 +437,23 @@ inline fun VaultQueryService.queryBy(criteria: Query return _queryBy(criteria, paging, sorting, T::class.java) } -inline fun VaultQueryService.trackBy(): Vault.PageAndUpdates { +inline fun VaultQueryService.trackBy(): DataFeed, Vault.Update> { return _trackBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), T::class.java) } -inline fun VaultQueryService.trackBy(criteria: QueryCriteria): Vault.PageAndUpdates { +inline fun VaultQueryService.trackBy(criteria: QueryCriteria): DataFeed, Vault.Update> { return _trackBy(criteria, PageSpecification(), Sort(emptySet()), T::class.java) } -inline fun VaultQueryService.trackBy(criteria: QueryCriteria, paging: PageSpecification): Vault.PageAndUpdates { +inline fun VaultQueryService.trackBy(criteria: QueryCriteria, paging: PageSpecification): DataFeed, Vault.Update> { return _trackBy(criteria, paging, Sort(emptySet()), T::class.java) } -inline fun VaultQueryService.trackBy(criteria: QueryCriteria, sorting: Sort): Vault.PageAndUpdates { +inline fun VaultQueryService.trackBy(criteria: QueryCriteria, sorting: Sort): DataFeed, Vault.Update> { return _trackBy(criteria, PageSpecification(), sorting, T::class.java) } -inline fun VaultQueryService.trackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): Vault.PageAndUpdates { +inline fun VaultQueryService.trackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): DataFeed, Vault.Update> { return _trackBy(criteria, paging, sorting, T::class.java) } diff --git a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt index 881ae4644d..f8d4f77378 100644 --- a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt @@ -68,9 +68,9 @@ class CordaRPCOpsImpl( override fun vaultTrackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, - contractType: Class): Vault.PageAndUpdates { + contractType: Class): DataFeed, Vault.Update> { return database.transaction { - services.vaultQueryService._trackBy(criteria, paging, sorting, contractType) + services.vaultQueryService._trackBy(criteria, paging, sorting, contractType) } } diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt index a014a3b545..4d43bd702d 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt @@ -7,6 +7,7 @@ import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef import net.corda.core.contracts.TransactionState import net.corda.core.crypto.SecureHash +import net.corda.core.messaging.DataFeed import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultQueryException import net.corda.core.node.services.VaultQueryService @@ -20,7 +21,6 @@ import net.corda.core.serialization.storageKryo import net.corda.core.utilities.loggerFor import net.corda.node.services.database.HibernateConfiguration import net.corda.node.services.vault.schemas.jpa.VaultSchemaV1 -import net.corda.node.utilities.wrapWithDatabaseTransaction import org.jetbrains.exposed.sql.transactions.TransactionManager import rx.subjects.PublishSubject import java.lang.Exception @@ -63,7 +63,7 @@ class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration, // pagination if (paging.pageNumber < 0) throw VaultQueryException("Page specification: invalid page number ${paging.pageNumber} [page numbers start from 0]") - if (paging.pageSize < 0 || paging.pageSize > MAX_PAGE_SIZE) throw VaultQueryException("Page specification: invalid page size ${paging.pageSize} [maximum page size is ${MAX_PAGE_SIZE}]") + if (paging.pageSize < 0 || paging.pageSize > MAX_PAGE_SIZE) throw VaultQueryException("Page specification: invalid page size ${paging.pageSize} [maximum page size is ${MAX_PAGE_SIZE}]") // count total results available val countQuery = criteriaBuilder.createQuery(Long::class.java) @@ -99,15 +99,14 @@ class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration, } } - private val mutex = ThreadBox ({ updatesPublisher }) + private val mutex = ThreadBox({ updatesPublisher }) @Throws(VaultQueryException::class) - override fun _trackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractType: Class): Vault.PageAndUpdates { + override fun _trackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractType: Class): DataFeed, Vault.Update> { return mutex.locked { val snapshotResults = _queryBy(criteria, paging, sorting, contractType) - Vault.PageAndUpdates(snapshotResults, - updatesPublisher.bufferUntilSubscribed() - .filter { it.containsType(contractType, snapshotResults.stateTypes) } ) + val updates = updatesPublisher.bufferUntilSubscribed().filter { it.containsType(contractType, snapshotResults.stateTypes) } + DataFeed(snapshotResults, updates) } } @@ -115,7 +114,7 @@ class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration, * Maintain a list of contract state interfaces to concrete types stored in the vault * for usage in generic queries of type queryBy or queryBy> */ - fun resolveUniqueContractStateTypes(session: EntityManager) : Map> { + fun resolveUniqueContractStateTypes(session: EntityManager): Map> { val criteria = criteriaBuilder.createQuery(String::class.java) val vaultStates = criteria.from(VaultSchemaV1.VaultStates::class.java) criteria.select(vaultStates.get("contractStateClassName")).distinct(true) @@ -135,7 +134,7 @@ class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration, return contractInterfaceToConcreteTypes } - private fun deriveContractInterfaces(clazz: Class): Set> { + private fun deriveContractInterfaces(clazz: Class): Set> { val myInterfaces: MutableSet> = mutableSetOf() clazz.interfaces.forEach { if (!it.equals(ContractState::class.java)) { diff --git a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java index 542c98a5db..d3daf97f05 100644 --- a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java +++ b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java @@ -7,6 +7,7 @@ import net.corda.contracts.asset.*; import net.corda.core.contracts.*; import net.corda.core.crypto.*; import net.corda.core.identity.*; +import net.corda.core.messaging.DataFeed; import net.corda.core.node.services.*; import net.corda.core.node.services.vault.*; import net.corda.core.node.services.vault.QueryCriteria.*; @@ -248,7 +249,7 @@ public class VaultQueryJavaTests { Set> contractStateTypes = new HashSet(Collections.singletonList(Cash.State.class)); VaultQueryCriteria criteria = new VaultQueryCriteria(Vault.StateStatus.UNCONSUMED, contractStateTypes); - Vault.PageAndUpdates results = vaultQuerySvc.trackBy(ContractState.class, criteria); + DataFeed, Vault.Update> results = vaultQuerySvc.trackBy(ContractState.class, criteria); Vault.Page snapshot = results.getCurrent(); Observable updates = results.getFuture(); @@ -289,7 +290,7 @@ public class VaultQueryJavaTests { PageSpecification pageSpec = new PageSpecification(0, getMAX_PAGE_SIZE()); Sort.SortColumn sortByUid = new Sort.SortColumn(new SortAttribute.Standard(Sort.LinearStateAttribute.UUID), Sort.Direction.DESC); Sort sorting = new Sort(ImmutableSet.of(sortByUid)); - Vault.PageAndUpdates results = vaultQuerySvc.trackBy(ContractState.class, compositeCriteria, pageSpec, sorting); + DataFeed, Vault.Update> results = vaultQuerySvc.trackBy(ContractState.class, compositeCriteria, pageSpec, sorting); Vault.Page snapshot = results.getCurrent(); Observable updates = results.getFuture(); From 83fdf678aba28d8e36ef419709851941ea324a2d Mon Sep 17 00:00:00 2001 From: Patrick Kuo Date: Wed, 28 Jun 2017 11:29:34 +0100 Subject: [PATCH 13/97] Update doc to show JMX no longer works in Corda, due to the serialisation changes (#922) * Update doc to show JMX no longer works in Corda, due to the serialisation changes. * minor changes * address PR issues --- docs/source/node-administration.rst | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/source/node-administration.rst b/docs/source/node-administration.rst index 99ea389fdf..6410ed4076 100644 --- a/docs/source/node-administration.rst +++ b/docs/source/node-administration.rst @@ -39,9 +39,13 @@ not require any particular network protocol for export. So this data can be expo some monitoring systems provide a "Java Agent", which is essentially a JVM plugin that finds all the MBeans and sends them out to a statistics collector over the network. For those systems, follow the instructions provided by the vendor. -Sometimes though, you just want raw access to the data and operations itself. So nodes export them over HTTP on the -``/monitoring/json`` HTTP endpoint, using a program called `Jolokia `_. Jolokia defines the JSON -and REST formats for accessing MBeans, and provides client libraries to work with that protocol as well. +.. warning:: As of Corda M11, Java serialisation in the Corda node has been restricted, meaning MBeans access via the JMX + port will no longer work. Please use java agents instead, you can find details on how to use Jolokia JVM + agent `here `_. + +`Jolokia `_ allows you to access the raw data and operations without connecting to the JMX port +directly. The nodes export the data over HTTP on the ``/jolokia`` HTTP endpoint, Jolokia defines the JSON and REST +formats for accessing MBeans, and provides client libraries to work with that protocol as well. Here are a few ways to build dashboards and extract monitoring data for a node: @@ -51,12 +55,11 @@ Here are a few ways to build dashboards and extract monitoring data for a node: * `JMXTrans `_ is another tool for Graphite, this time, it's got its own agent (JVM plugin) which reads a custom config file and exports only the named data. It's more configurable than JMX2Graphite and doesn't require a separate process, as the JVM will write directly to Graphite. -* *Java Mission Control* is a desktop app that can connect to a target JVM that has the right command line flags set - (or always, if running locally). You can explore what data is available, create graphs of those metrics, and invoke - management operations like forcing a garbage collection. -* *VisualVM* is another desktop app that can do fine grained JVM monitoring and sampling. Very useful during development. * Cloud metrics services like New Relic also understand JMX, typically, by providing their own agent that uploads the data to their service on a regular schedule. +* `Telegraf `_ is a tool to collect, process, aggregate, and write metrics. + It can bridge any data input to any output using their plugin system, for example, Telegraf can + be configured to collect data from Jolokia and write to DataDog web api. Memory usage and tuning ----------------------- From e5395fe1b7c8a0918684c27e3d6ddec5a9dddbff Mon Sep 17 00:00:00 2001 From: Andrzej Cichocki Date: Wed, 28 Jun 2017 12:07:53 +0100 Subject: [PATCH 14/97] Enforce node death on failure to register with network map (#905) * Give up polling when result future cancelled --- .../java/net/corda/cordform/CordformNode.java | 2 +- .../net/corda/cordform/NodeDefinition.java | 9 ++ core/src/main/kotlin/net/corda/core/Utils.kt | 2 +- .../corda/core/concurrent/ConcurrencyUtils.kt | 37 +++++++ .../core/concurrent/ConcurrencyUtilsTest.kt | 78 +++++++++++++++ .../kotlin/net/corda/node/BootTests.kt | 14 +++ .../net/corda/node/internal/NodeStartup.kt | 3 - .../services/messaging/NodeMessagingClient.kt | 22 +++-- .../kotlin/net/corda/testing/driver/Driver.kt | 97 +++++++++++-------- 9 files changed, 208 insertions(+), 56 deletions(-) create mode 100644 cordform-common/src/main/java/net/corda/cordform/NodeDefinition.java create mode 100644 core/src/main/kotlin/net/corda/core/concurrent/ConcurrencyUtils.kt create mode 100644 core/src/test/kotlin/net/corda/core/concurrent/ConcurrencyUtilsTest.kt diff --git a/cordform-common/src/main/java/net/corda/cordform/CordformNode.java b/cordform-common/src/main/java/net/corda/cordform/CordformNode.java index 66e0ba9ca0..80a9a3795a 100644 --- a/cordform-common/src/main/java/net/corda/cordform/CordformNode.java +++ b/cordform-common/src/main/java/net/corda/cordform/CordformNode.java @@ -7,7 +7,7 @@ import com.typesafe.config.ConfigValueFactory; import java.util.List; import java.util.Map; -public class CordformNode { +public class CordformNode implements NodeDefinition { protected static final String DEFAULT_HOST = "localhost"; /** diff --git a/cordform-common/src/main/java/net/corda/cordform/NodeDefinition.java b/cordform-common/src/main/java/net/corda/cordform/NodeDefinition.java new file mode 100644 index 0000000000..0b86b98627 --- /dev/null +++ b/cordform-common/src/main/java/net/corda/cordform/NodeDefinition.java @@ -0,0 +1,9 @@ +package net.corda.cordform; + +import com.typesafe.config.Config; + +public interface NodeDefinition { + String getName(); + + Config getConfig(); +} diff --git a/core/src/main/kotlin/net/corda/core/Utils.kt b/core/src/main/kotlin/net/corda/core/Utils.kt index 2c26c227c2..021e6e194d 100644 --- a/core/src/main/kotlin/net/corda/core/Utils.kt +++ b/core/src/main/kotlin/net/corda/core/Utils.kt @@ -362,7 +362,7 @@ data class ErrorOr private constructor(val value: A?, val error: Throwabl companion object { /** Runs the given lambda and wraps the result. */ - inline fun catch(body: () -> T): ErrorOr { + inline fun catch(body: () -> T): ErrorOr { return try { ErrorOr(body()) } catch (t: Throwable) { diff --git a/core/src/main/kotlin/net/corda/core/concurrent/ConcurrencyUtils.kt b/core/src/main/kotlin/net/corda/core/concurrent/ConcurrencyUtils.kt new file mode 100644 index 0000000000..11750fe3e8 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/concurrent/ConcurrencyUtils.kt @@ -0,0 +1,37 @@ +package net.corda.core.concurrent + +import com.google.common.annotations.VisibleForTesting +import com.google.common.util.concurrent.ListenableFuture +import com.google.common.util.concurrent.SettableFuture +import net.corda.core.catch +import net.corda.core.failure +import net.corda.core.then +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.util.concurrent.atomic.AtomicBoolean + +/** + * As soon as a given future becomes done, the handler is invoked with that future as its argument. + * The result of the handler is copied into the result future, and the handler isn't invoked again. + * If a given future errors after the result future is done, the error is automatically logged. + */ +fun firstOf(vararg futures: ListenableFuture, handler: (ListenableFuture) -> T) = firstOf(futures, defaultLog, handler) + +private val defaultLog = LoggerFactory.getLogger("net.corda.core.concurrent") +@VisibleForTesting +internal val shortCircuitedTaskFailedMessage = "Short-circuited task failed:" + +internal fun firstOf(futures: Array>, log: Logger, handler: (ListenableFuture) -> T): ListenableFuture { + val resultFuture = SettableFuture.create() + val winnerChosen = AtomicBoolean() + futures.forEach { + it.then { + if (winnerChosen.compareAndSet(false, true)) { + resultFuture.catch { handler(it) } + } else if (!it.isCancelled) { + it.failure { log.error(shortCircuitedTaskFailedMessage, it) } + } + } + } + return resultFuture +} diff --git a/core/src/test/kotlin/net/corda/core/concurrent/ConcurrencyUtilsTest.kt b/core/src/test/kotlin/net/corda/core/concurrent/ConcurrencyUtilsTest.kt new file mode 100644 index 0000000000..722d67184e --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/concurrent/ConcurrencyUtilsTest.kt @@ -0,0 +1,78 @@ +package net.corda.core.concurrent + +import com.google.common.util.concurrent.SettableFuture +import com.nhaarman.mockito_kotlin.* +import net.corda.core.getOrThrow +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.Test +import org.slf4j.Logger +import java.io.EOFException +import java.util.concurrent.CancellationException +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class ConcurrencyUtilsTest { + private val f1 = SettableFuture.create() + private val f2 = SettableFuture.create() + private var invocations = 0 + private val log: Logger = mock() + @Test + fun `firstOf short circuit`() { + // Order not significant in this case: + val g = firstOf(arrayOf(f2, f1), log) { + ++invocations + it.getOrThrow() + } + f1.set(100) + assertEquals(100, g.getOrThrow()) + assertEquals(1, invocations) + verifyNoMoreInteractions(log) + val throwable = EOFException("log me") + f2.setException(throwable) + assertEquals(1, invocations) // Least astonishing to skip handler side-effects. + verify(log).error(eq(shortCircuitedTaskFailedMessage), same(throwable)) + } + + @Test + fun `firstOf re-entrant handler attempt due to cancel`() { + val futures = arrayOf(f1, f2) + val g = firstOf(futures, log) { + ++invocations + futures.forEach { it.cancel(false) } // One handler invocation queued here. + it.getOrThrow() + } + f1.set(100) + assertEquals(100, g.getOrThrow()) + assertEquals(1, invocations) // Handler didn't run as g was already done. + verifyNoMoreInteractions(log) // CancellationException is not logged (if due to cancel). + assertTrue(f2.isCancelled) + } + + @Test + fun `firstOf re-entrant handler attempt not due to cancel`() { + val futures = arrayOf(f1, f2) + val fakeCancel = CancellationException() + val g = firstOf(futures, log) { + ++invocations + futures.forEach { it.setException(fakeCancel) } // One handler attempt here. + it.getOrThrow() + } + f1.set(100) + assertEquals(100, g.getOrThrow()) + assertEquals(1, invocations) // Handler didn't run as g was already done. + verify(log).error(eq(shortCircuitedTaskFailedMessage), same(fakeCancel)) + assertThatThrownBy { f2.getOrThrow() }.isSameAs(fakeCancel) + } + + @Test + fun `firstOf cancel is not special`() { + val g = firstOf(arrayOf(f2, f1), log) { + ++invocations + it.getOrThrow() // This can always do something fancy if 'it' was cancelled. + } + f1.cancel(false) + assertThatThrownBy { g.getOrThrow() }.isInstanceOf(CancellationException::class.java) + assertEquals(1, invocations) + verifyNoMoreInteractions(log) + } +} diff --git a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt index f356852b09..d2f8ebff8e 100644 --- a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt @@ -6,11 +6,15 @@ import net.corda.core.flows.FlowLogic import net.corda.core.flows.StartableByRPC import net.corda.core.getOrThrow import net.corda.core.messaging.startFlow +import net.corda.core.node.services.ServiceInfo +import net.corda.core.node.services.ServiceType import net.corda.core.utilities.ALICE import net.corda.testing.driver.driver import net.corda.node.internal.NodeStartup import net.corda.node.services.startFlowPermission import net.corda.nodeapi.User +import net.corda.testing.driver.ListenProcessDeathException +import net.corda.testing.driver.NetworkMapStartStrategy import net.corda.testing.ProjectStructure.projectRootDir import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy @@ -18,6 +22,7 @@ import org.junit.Test import java.io.* import java.nio.file.Files import kotlin.test.assertEquals +import kotlin.test.assertFailsWith class BootTests { @@ -48,6 +53,15 @@ class BootTests { assertEquals(1, numberOfNodesThatLogged) } } + + @Test + fun `node quits on failure to register with network map`() { + val tooManyAdvertisedServices = (1..100).map { ServiceInfo(ServiceType.regulator.getSubType("$it")) }.toSet() + driver(networkMapStartStrategy = NetworkMapStartStrategy.Nominated(ALICE.name)) { + val future = startNode(ALICE.name, advertisedServices = tooManyAdvertisedServices) + assertFailsWith(ListenProcessDeathException::class) { future.getOrThrow() } + } + } } @StartableByRPC diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt index 095318a92f..937e61d18e 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -118,9 +118,6 @@ open class NodeStartup(val args: Array) { logger.error("Shell failed to start", e) } } - } failure { - logger.error("Error during network map registration", it) - exitProcess(1) } node.run() } diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt index 9bc17e894c..bb46e06be6 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt @@ -2,7 +2,7 @@ package net.corda.node.services.messaging import com.google.common.net.HostAndPort import com.google.common.util.concurrent.ListenableFuture -import net.corda.core.ThreadBox +import net.corda.core.* import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.MessageRecipients import net.corda.core.messaging.RPCOps @@ -10,9 +10,7 @@ import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.VersionInfo import net.corda.core.node.services.PartyInfo import net.corda.core.node.services.TransactionVerifierService -import net.corda.core.random63BitValue import net.corda.core.serialization.opaque -import net.corda.core.success import net.corda.core.transactions.LedgerTransaction import net.corda.core.utilities.loggerFor import net.corda.core.utilities.trace @@ -236,7 +234,7 @@ class NodeMessagingClient(override val config: NodeConfiguration, } } - private var shutdownLatch = CountDownLatch(1) + private val shutdownLatch = CountDownLatch(1) private fun processMessage(consumer: ClientConsumer): Boolean { // Two possibilities here: @@ -286,6 +284,9 @@ class NodeMessagingClient(override val config: NodeConfiguration, while (!networkMapRegistrationFuture.isDone && processMessage(consumer)) { } + with(networkMapRegistrationFuture) { + if (isDone) getOrThrow() else andForget(log) // Trigger node shutdown here to avoid deadlock in shutdown hooks. + } } private fun runPostNetworkMap() { @@ -306,11 +307,14 @@ class NodeMessagingClient(override val config: NodeConfiguration, * consume all messages via a new consumer without a filter applied. */ fun run(serverControl: ActiveMQServerControl) { - // Build the network map. - runPreNetworkMap(serverControl) - // Process everything else once we have the network map. - runPostNetworkMap() - shutdownLatch.countDown() + try { + // Build the network map. + runPreNetworkMap(serverControl) + // Process everything else once we have the network map. + runPostNetworkMap() + } finally { + shutdownLatch.countDown() + } } private fun artemisToCordaMessage(message: ClientMessage): ReceivedMessage? { diff --git a/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt b/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt index 4667a52bef..e90b25a655 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -9,7 +9,9 @@ import com.typesafe.config.ConfigRenderOptions import net.corda.client.rpc.CordaRPCClient import net.corda.cordform.CordformContext import net.corda.cordform.CordformNode +import net.corda.cordform.NodeDefinition import net.corda.core.* +import net.corda.core.concurrent.firstOf import net.corda.core.crypto.X509Utilities import net.corda.core.crypto.appendToCommonName import net.corda.core.crypto.commonName @@ -31,7 +33,6 @@ import net.corda.nodeapi.ArtemisMessagingComponent import net.corda.nodeapi.User import net.corda.nodeapi.config.SSLConfiguration import net.corda.nodeapi.config.parseAs -import net.corda.nodeapi.internal.ShutdownHook import net.corda.nodeapi.internal.addShutdownHook import net.corda.testing.MOCK_VERSION_INFO import okhttp3.OkHttpClient @@ -275,19 +276,16 @@ fun genericD coerce: (D) -> DI, dsl: DI.() -> A ): A { - var shutdownHook: ShutdownHook? = null + val shutdownHook = addShutdownHook(driverDsl::shutdown) try { driverDsl.start() - shutdownHook = addShutdownHook { - driverDsl.shutdown() - } return dsl(coerce(driverDsl)) } catch (exception: Throwable) { log.error("Driver shutting down because of exception", exception) throw exception } finally { driverDsl.shutdown() - shutdownHook?.cancel() + shutdownHook.cancel() } } @@ -295,7 +293,7 @@ fun getTimestampAsDirectoryName(): String { return DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(UTC).format(Instant.now()) } -class ListenProcessDeathException(message: String) : Exception(message) +class ListenProcessDeathException(hostAndPort: HostAndPort, listenProcess: Process) : Exception("The process that was expected to listen on $hostAndPort has died with status: ${listenProcess.exitValue()}") /** * @throws ListenProcessDeathException if [listenProcess] dies before the check succeeds, i.e. the check can't succeed as intended. @@ -307,7 +305,7 @@ fun addressMustBeBound(executorService: ScheduledExecutorService, hostAndPort: H fun addressMustBeBoundFuture(executorService: ScheduledExecutorService, hostAndPort: HostAndPort, listenProcess: Process? = null): ListenableFuture { return poll(executorService, "address $hostAndPort to bind") { if (listenProcess != null && !listenProcess.isAlive) { - throw ListenProcessDeathException("The process that was expected to listen on $hostAndPort has died with status: ${listenProcess.exitValue()}") + throw ListenProcessDeathException(hostAndPort, listenProcess) } try { Socket(hostAndPort.host, hostAndPort.port).close() @@ -340,33 +338,26 @@ fun poll( warnCount: Int = 120, check: () -> A? ): ListenableFuture { - val initialResult = check() val resultFuture = SettableFuture.create() - if (initialResult != null) { - resultFuture.set(initialResult) - return resultFuture - } - var counter = 0 - fun schedulePoll() { - executorService.schedule(task@ { - counter++ - if (counter == warnCount) { + val task = object : Runnable { + var counter = -1 + override fun run() { + if (resultFuture.isCancelled) return // Give up, caller can no longer get the result. + if (++counter == warnCount) { log.warn("Been polling $pollName for ${pollInterval.multipliedBy(warnCount.toLong()).seconds} seconds...") } - val result = try { - check() - } catch (t: Throwable) { - resultFuture.setException(t) - return@task - } - if (result == null) { - schedulePoll() - } else { - resultFuture.set(result) - } - }, pollInterval.toMillis(), MILLISECONDS) + ErrorOr.catch(check).match(onValue = { + if (it != null) { + resultFuture.set(it) + } else { + executorService.schedule(this, pollInterval.toMillis(), MILLISECONDS) + } + }, onError = { + resultFuture.setException(it) + }) + } } - schedulePoll() + executorService.submit(task) // The check may be expensive, so always run it in the background even the first time. return resultFuture } @@ -518,21 +509,28 @@ class DriverDSL( _executorService?.shutdownNow() } - private fun establishRpc(nodeAddress: HostAndPort, sslConfig: SSLConfiguration): ListenableFuture { + private fun establishRpc(nodeAddress: HostAndPort, sslConfig: SSLConfiguration, processDeathFuture: ListenableFuture): ListenableFuture { val client = CordaRPCClient(nodeAddress, sslConfig) - return poll(executorService, "for RPC connection") { + val connectionFuture = poll(executorService, "RPC connection") { try { - val connection = client.start(ArtemisMessagingComponent.NODE_USER, ArtemisMessagingComponent.NODE_USER) - shutdownManager.registerShutdown { connection.close() } - return@poll connection.proxy - } catch(e: Exception) { + client.start(ArtemisMessagingComponent.NODE_USER, ArtemisMessagingComponent.NODE_USER) + } catch (e: Exception) { + if (processDeathFuture.isDone) throw e log.error("Exception $e, Retrying RPC connection at $nodeAddress") null } } + return firstOf(connectionFuture, processDeathFuture) { + if (it == processDeathFuture) { + throw processDeathFuture.getOrThrow() + } + val connection = connectionFuture.getOrThrow() + shutdownManager.registerShutdown(connection::close) + connection.proxy + } } - private fun networkMapServiceConfigLookup(networkMapCandidates: List): (X500Name) -> Map? { + private fun networkMapServiceConfigLookup(networkMapCandidates: List): (X500Name) -> Map? { return networkMapStartStrategy.run { when (this) { is NetworkMapStartStrategy.Dedicated -> { @@ -564,6 +562,10 @@ class DriverDSL( val webAddress = portAllocation.nextHostAndPort() // TODO: Derive name from the full picked name, don't just wrap the common name val name = providedName ?: X509Utilities.getX509Name("${oneOf(names).commonName}-${p2pAddress.port}","London","demo@r3.com",null) + val networkMapServiceConfigLookup = networkMapServiceConfigLookup(listOf(object : NodeDefinition { + override fun getName() = name.toString() + override fun getConfig() = configOf("p2pAddress" to p2pAddress.toString()) + })) val config = ConfigHelper.loadConfig( baseDirectory = baseDirectory(name), allowMissingConfig = true, @@ -573,7 +575,7 @@ class DriverDSL( "rpcAddress" to rpcAddress.toString(), "webAddress" to webAddress.toString(), "extraAdvertisedServiceIds" to advertisedServices.map { it.toString() }, - "networkMapService" to networkMapServiceConfigLookup(emptyList())(name), + "networkMapService" to networkMapServiceConfigLookup(name), "useTestClock" to useTestClock, "rpcUsers" to rpcUsers.map { it.toMap() }, "verifierType" to verifierType.name @@ -708,7 +710,7 @@ class DriverDSL( } } ) return nodeAndThreadFuture.flatMap { (node, thread) -> - establishRpc(nodeConfiguration.p2pAddress, nodeConfiguration).flatMap { rpc -> + establishRpc(nodeConfiguration.p2pAddress, nodeConfiguration, SettableFuture.create()).flatMap { rpc -> rpc.waitUntilRegisteredWithNetworkMap().map { NodeHandle.InProcess(rpc.nodeIdentity(), rpc, nodeConfiguration, webAddress, node, thread) } @@ -719,9 +721,20 @@ class DriverDSL( val processFuture = startOutOfProcessNode(executorService, nodeConfiguration, config, quasarJarPath, debugPort, systemProperties, callerPackage) registerProcess(processFuture) return processFuture.flatMap { process -> + val processDeathFuture = poll(executorService, "process death") { + if (process.isAlive) null else ListenProcessDeathException(nodeConfiguration.p2pAddress, process) + } // We continue to use SSL enabled port for RPC when its for node user. - establishRpc(nodeConfiguration.p2pAddress, nodeConfiguration).flatMap { rpc -> - rpc.waitUntilRegisteredWithNetworkMap().map { + establishRpc(nodeConfiguration.p2pAddress, nodeConfiguration, processDeathFuture).flatMap { rpc -> + // Call waitUntilRegisteredWithNetworkMap in background in case RPC is failing over: + val networkMapFuture = executorService.submit(Callable { + rpc.waitUntilRegisteredWithNetworkMap() + }).flatMap { it } + firstOf(processDeathFuture, networkMapFuture) { + if (it == processDeathFuture) { + throw processDeathFuture.getOrThrow() + } + processDeathFuture.cancel(false) NodeHandle.OutOfProcess(rpc.nodeIdentity(), rpc, nodeConfiguration, webAddress, debugPort, process) } } From 1a4965c29464624bdc26b1fc16b6034307e9c5e3 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Wed, 28 Jun 2017 13:47:50 +0100 Subject: [PATCH 15/97] Change CashIssueFlow to use anonymous identity * Add functions for: * Retrieving nodes via their legal identity * Filtering a set of public keys down to those the node has corresponding private keys for * Modify contract upgrade flows to handle identifying participants after an anomymisation step * Correct terminology: "party who" -> "party which" * Modify CashIssueFlow and CashPaymentFlow to optionally use an anonymous identity for the recipient. --- .../corda/client/jfx/NodeMonitorModelTest.kt | 9 ++-- .../net/corda/client/mock/EventGenerator.kt | 8 +-- .../corda/core/identity/AnonymisedIdentity.kt | 16 ++++++ .../net/corda/core/messaging/CordaRPCOps.kt | 10 ++++ .../core/node/services/NetworkMapCache.kt | 13 +++++ .../net/corda/core/node/services/Services.kt | 13 ++++- .../serialization/DefaultKryoCustomizer.kt | 3 +- .../net/corda/core/serialization/Kryo.kt | 14 +++++ .../flows/AbstractStateReplacementFlow.kt | 27 +++++++--- .../net/corda/flows/ContractUpgradeFlow.kt | 9 ++-- .../net/corda/flows/NotaryChangeFlow.kt | 16 +++--- .../main/kotlin/net/corda/flows/TxKeyFlow.kt | 54 ++++++++++--------- .../core/flows/ContractUpgradeFlowTest.kt | 6 ++- .../kotlin/net/corda/flows/TxKeyFlowTests.kt | 2 +- .../corda/docs/IntegrationTestingTutorial.kt | 5 +- .../corda/docs/FxTransactionBuildTutorial.kt | 22 ++++---- .../docs/FxTransactionBuildTutorialTest.kt | 6 ++- .../net/corda/contracts/asset/Obligation.kt | 4 +- .../corda/contracts/asset/OnLedgerAsset.kt | 6 ++- .../net/corda/flows/AbstractCashFlow.kt | 20 +++++-- .../kotlin/net/corda/flows/CashExitFlow.kt | 11 ++-- .../kotlin/net/corda/flows/CashFlowCommand.kt | 13 ++--- .../kotlin/net/corda/flows/CashIssueFlow.kt | 33 ++++++++---- .../kotlin/net/corda/flows/CashPaymentFlow.kt | 28 +++++++--- .../main/kotlin/net/corda/flows/IssuerFlow.kt | 51 +++++++++++++----- .../net/corda/flows/CashExitFlowTests.kt | 4 +- .../net/corda/flows/CashIssueFlowTests.kt | 4 +- .../net/corda/flows/CashPaymentFlowTests.kt | 12 +++-- .../kotlin/net/corda/flows/IssuerFlowTest.kt | 17 ++++-- .../net/corda/node/internal/AbstractNode.kt | 4 +- .../keys/E2ETestKeyManagementService.kt | 8 ++- .../net/corda/node/services/keys/KMSUtils.kt | 5 +- .../keys/PersistentKeyManagementService.kt | 7 ++- .../network/InMemoryNetworkMapCache.kt | 20 ++++++- .../net/corda/node/CordaRPCOpsImplTest.kt | 39 +++++++------- .../node/services/MockServiceHubInternal.kt | 4 +- .../messaging/ArtemisMessagingTests.kt | 3 +- .../network/InMemoryIdentityServiceTests.kt | 5 +- .../network/InMemoryNetworkMapCacheTest.kt | 22 ++++++++ .../statemachine/FlowFrameworkTests.kt | 5 +- .../net/corda/bank/BankOfCordaHttpAPITest.kt | 3 +- .../corda/bank/BankOfCordaRPCClientTest.kt | 13 +++-- .../net/corda/bank/BankOfCordaDriver.kt | 3 +- .../corda/bank/api/BankOfCordaClientApi.kt | 2 +- .../net/corda/bank/api/BankOfCordaWebApi.kt | 6 ++- .../net/corda/vega/flows/StateRevisionFlow.kt | 8 ++- .../net/corda/traderdemo/TraderDemoTest.kt | 3 +- .../corda/traderdemo/TraderDemoClientApi.kt | 4 +- .../corda/testing/node/MockNetworkMapCache.kt | 3 +- .../net/corda/testing/node/MockServices.kt | 5 +- .../net/corda/testing/node/SimpleNode.kt | 3 +- .../net/corda/explorer/ExplorerSimulation.kt | 14 +++-- .../views/cordapps/cash/NewTransaction.kt | 23 +++++--- .../net/corda/loadtest/tests/CrossCashTest.kt | 5 +- .../corda/loadtest/tests/GenerateHelpers.kt | 10 ++-- .../net/corda/loadtest/tests/SelfIssueTest.kt | 2 +- .../net/corda/loadtest/tests/StabilityTest.kt | 4 +- 57 files changed, 464 insertions(+), 205 deletions(-) create mode 100644 core/src/main/kotlin/net/corda/core/identity/AnonymisedIdentity.kt diff --git a/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt b/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt index 678f4b3a50..4d76aaa282 100644 --- a/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt +++ b/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt @@ -113,11 +113,13 @@ class NodeMonitorModelTest : DriverBasedTest() { @Test fun `cash issue works end to end`() { + val anonymous = false rpc.startFlow(::CashIssueFlow, Amount(100, USD), OpaqueBytes(ByteArray(1, { 1 })), aliceNode.legalIdentity, - notaryNode.notaryIdentity + notaryNode.notaryIdentity, + anonymous ) vaultUpdates.expectEvents(isStrict = false) { @@ -138,8 +140,9 @@ class NodeMonitorModelTest : DriverBasedTest() { @Test fun `cash issue and move`() { - rpc.startFlow(::CashIssueFlow, 100.DOLLARS, OpaqueBytes.of(1), aliceNode.legalIdentity, notaryNode.notaryIdentity).returnValue.getOrThrow() - rpc.startFlow(::CashPaymentFlow, 100.DOLLARS, bobNode.legalIdentity).returnValue.getOrThrow() + val anonymous = false + rpc.startFlow(::CashIssueFlow, 100.DOLLARS, OpaqueBytes.of(1), aliceNode.legalIdentity, notaryNode.notaryIdentity, anonymous).returnValue.getOrThrow() + rpc.startFlow(::CashPaymentFlow, 100.DOLLARS, bobNode.legalIdentity, anonymous).returnValue.getOrThrow() var issueSmId: StateMachineRunId? = null var moveSmId: StateMachineRunId? = null diff --git a/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt b/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt index 771fca5fd1..7c4503fb36 100644 --- a/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt +++ b/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt @@ -26,7 +26,7 @@ open class EventGenerator(val parties: List, val currencies: List addToMap(ccy, amount) - CashFlowCommand.IssueCash(Amount(amount, ccy), issueRef, to, notary) + CashFlowCommand.IssueCash(Amount(amount, ccy), issueRef, to, notary, anonymous = true) } protected val exitCashGenerator = amountGenerator.combine(issueRefGenerator, currencyGenerator) { amount, issueRef, ccy -> @@ -35,7 +35,7 @@ open class EventGenerator(val parties: List, val currencies: List - CashFlowCommand.PayCash(Amount(amountIssued, currency), recipient) + CashFlowCommand.PayCash(Amount(amountIssued, currency), recipient, anonymous = true) } open val issuerGenerator = Generator.frequency(listOf( @@ -71,11 +71,11 @@ class ErrorFlowsEventGenerator(parties: List, currencies: List, } val normalMoveGenerator = amountGenerator.combine(partyGenerator, currencyGenerator) { amountIssued, recipient, currency -> - CashFlowCommand.PayCash(Amount(amountIssued, currency), recipient) + CashFlowCommand.PayCash(Amount(amountIssued, currency), recipient, anonymous = true) } val errorMoveGenerator = partyGenerator.combine(currencyGenerator) { recipient, currency -> - CashFlowCommand.PayCash(Amount(currencyMap[currency]!! * 2, currency), recipient) + CashFlowCommand.PayCash(Amount(currencyMap[currency]!! * 2, currency), recipient, anonymous = true) } override val moveCashGenerator = Generator.frequency(listOf( diff --git a/core/src/main/kotlin/net/corda/core/identity/AnonymisedIdentity.kt b/core/src/main/kotlin/net/corda/core/identity/AnonymisedIdentity.kt new file mode 100644 index 0000000000..0048917443 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/identity/AnonymisedIdentity.kt @@ -0,0 +1,16 @@ +package net.corda.flows + +import net.corda.core.identity.AnonymousParty +import net.corda.core.serialization.CordaSerializable +import org.bouncycastle.cert.X509CertificateHolder +import java.security.PublicKey +import java.security.cert.CertPath + +@CordaSerializable +data class AnonymisedIdentity( + val certPath: CertPath, + val certificate: X509CertificateHolder, + val identity: AnonymousParty) { + constructor(certPath: CertPath, certificate: X509CertificateHolder, identity: PublicKey) + : this(certPath, certificate, AnonymousParty(identity)) +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt index cc79f7e26a..7392201658 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt @@ -350,6 +350,16 @@ inline fun > CordaRPCOps.startFlow arg3: D ): FlowHandle = startFlowDynamic(R::class.java, arg0, arg1, arg2, arg3) +inline fun > CordaRPCOps.startFlow( + @Suppress("UNUSED_PARAMETER") + flowConstructor: (A, B, C, D, E) -> R, + arg0: A, + arg1: B, + arg2: C, + arg3: D, + arg4: E +): FlowHandle = startFlowDynamic(R::class.java, arg0, arg1, arg2, arg3, arg4) + /** * Same again, except this time with progress-tracking enabled. */ diff --git a/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt b/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt index 7713bc35bb..cda2fc7c5f 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt @@ -2,9 +2,11 @@ package net.corda.core.node.services import com.google.common.util.concurrent.ListenableFuture import net.corda.core.contracts.Contract +import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.messaging.DataFeed import net.corda.core.node.NodeInfo +import net.corda.core.node.ServiceHub import net.corda.core.randomOrNull import net.corda.core.serialization.CordaSerializable import org.bouncycastle.asn1.x500.X500Name @@ -63,6 +65,17 @@ interface NetworkMapCache { */ fun getRecommended(type: ServiceType, contract: Contract, vararg party: Party): NodeInfo? = getNodesWithService(type).firstOrNull() + /** + * Look up the node info for a specific party. Will attempt to de-anonymise the party if applicable; if the party + * is anonymised and the well known party cannot be resolved, it is impossible ot identify the node and therefore this + * returns null. + * + * @param party party to retrieve node information for. + * @return the node for the identity, or null if the node could not be found. This does not necessarily mean there is + * no node for the party, only that this cache is unaware of it. + */ + fun getNodeByLegalIdentity(party: AbstractParty): NodeInfo? + /** Look up the node info for a legal name. */ fun getNodeByLegalName(principal: X500Name): NodeInfo? = partyNodes.singleOrNull { it.legalIdentity.name == principal } diff --git a/core/src/main/kotlin/net/corda/core/node/services/Services.kt b/core/src/main/kotlin/net/corda/core/node/services/Services.kt index 432140cf8f..39d8d53a4f 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/Services.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/Services.kt @@ -21,6 +21,7 @@ import net.corda.core.toFuture import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction +import net.corda.flows.AnonymisedIdentity import org.bouncycastle.cert.X509CertificateHolder import rx.Observable import rx.subjects.PublishSubject @@ -486,9 +487,17 @@ interface KeyManagementService { * @return X.509 certificate and path to the trust root. */ @Suspendable - fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): Pair + fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): AnonymisedIdentity - /** Using the provided signing [PublicKey] internally looks up the matching [PrivateKey] and signs the data. + /** + * Filter some keys down to the set that this node owns (has private keys for). + * + * @param candidateKeys keys which this node may own. + */ + fun filterMyKeys(candidateKeys: Iterable): Iterable + + /** + * Using the provided signing [PublicKey] internally looks up the matching [PrivateKey] and signs the data. * @param bytes The data to sign over using the chosen key. * @param publicKey The [PublicKey] partner to an internally held [PrivateKey], either derived from the node's primary identity, * or previously generated via the [freshKey] method. diff --git a/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt b/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt index 7963fb549e..3f903409fa 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt @@ -73,7 +73,7 @@ object DefaultKryoCustomizer { noReferencesWithin() - register(sun.security.ec.ECPublicKeyImpl::class.java, PublicKeySerializer) + register(sun.security.ec.ECPublicKeyImpl::class.java, ECPublicKeyImplSerializer) register(EdDSAPublicKey::class.java, Ed25519PublicKeySerializer) register(EdDSAPrivateKey::class.java, Ed25519PrivateKeySerializer) @@ -113,6 +113,7 @@ object DefaultKryoCustomizer { register(BCRSAPublicKey::class.java, PublicKeySerializer) register(BCSphincs256PrivateKey::class.java, PrivateKeySerializer) register(BCSphincs256PublicKey::class.java, PublicKeySerializer) + register(sun.security.ec.ECPublicKeyImpl::class.java, PublicKeySerializer) val customization = KryoSerializationCustomization(this) pluginRegistries.forEach { it.customizeSerialization(customization) } diff --git a/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt b/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt index 16e73c836b..3289724162 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt @@ -387,6 +387,20 @@ object Ed25519PublicKeySerializer : Serializer() { } } +/** For serialising an ed25519 public key */ +@ThreadSafe +object ECPublicKeyImplSerializer : Serializer() { + override fun write(kryo: Kryo, output: Output, obj: sun.security.ec.ECPublicKeyImpl) { + output.writeBytesWithLength(obj.encoded) + } + + override fun read(kryo: Kryo, input: Input, type: Class): sun.security.ec.ECPublicKeyImpl { + val A = input.readBytesWithLength() + val der = sun.security.util.DerValue(A) + return sun.security.ec.ECPublicKeyImpl.parse(der) as sun.security.ec.ECPublicKeyImpl + } +} + // TODO Implement standardized serialization of CompositeKeys. See JIRA issue: CORDA-249. @ThreadSafe object CompositeKeySerializer : Serializer() { diff --git a/core/src/main/kotlin/net/corda/flows/AbstractStateReplacementFlow.kt b/core/src/main/kotlin/net/corda/flows/AbstractStateReplacementFlow.kt index f00941e786..a035bd256f 100644 --- a/core/src/main/kotlin/net/corda/flows/AbstractStateReplacementFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/AbstractStateReplacementFlow.kt @@ -9,6 +9,7 @@ import net.corda.core.crypto.isFulfilledBy import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic import net.corda.core.identity.AbstractParty +import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction @@ -33,6 +34,15 @@ abstract class AbstractStateReplacementFlow { @CordaSerializable data class Proposal(val stateRef: StateRef, val modification: M, val stx: SignedTransaction) + /** + * The assembled transaction for upgrading a contract. + * + * @param stx signed transaction to do the upgrade. + * @param participants the parties involved in the upgrade transaction. + * @param myKey key + */ + data class UpgradeTx(val stx: SignedTransaction, val participants: Iterable, val myKey: PublicKey) + /** * The [Instigator] assembles the transaction for state replacement and sends out change proposals to all participants * ([Acceptor]) of that state. If participants agree to the proposed change, they each sign the transaction. @@ -57,17 +67,14 @@ abstract class AbstractStateReplacementFlow { @Suspendable @Throws(StateReplacementException::class) override fun call(): StateAndRef { - val (stx, participants) = assembleTx() + val (stx, participantKeys, myKey) = assembleTx() progressTracker.currentStep = SIGNING - val myKey = serviceHub.myInfo.legalIdentity - val me = listOf(myKey) - - val signatures = if (participants == me) { + val signatures = if (participantKeys.singleOrNull() == myKey) { getNotarySignatures(stx) } else { - collectSignatures((participants - me).map { it.owningKey }, stx) + collectSignatures(participantKeys - myKey, stx) } val finalTx = stx + signatures @@ -75,7 +82,13 @@ abstract class AbstractStateReplacementFlow { return finalTx.tx.outRef(0) } - abstract protected fun assembleTx(): Pair> + /** + * Build the upgrade transaction. + * + * @return a triple of the transaction, the public keys of all participants, and the participating public key of + * this node. + */ + abstract protected fun assembleTx(): UpgradeTx @Suspendable private fun collectSignatures(participants: Iterable, stx: SignedTransaction): List { diff --git a/core/src/main/kotlin/net/corda/flows/ContractUpgradeFlow.kt b/core/src/main/kotlin/net/corda/flows/ContractUpgradeFlow.kt index e876bcabbd..ce785ed6f2 100644 --- a/core/src/main/kotlin/net/corda/flows/ContractUpgradeFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/ContractUpgradeFlow.kt @@ -58,9 +58,12 @@ class ContractUpgradeFlow> { + override fun assembleTx(): AbstractStateReplacementFlow.UpgradeTx { val baseTx = assembleBareTx(originalState, modification) - val stx = serviceHub.signInitialTransaction(baseTx) - return stx to originalState.state.data.participants + val participantKeys = originalState.state.data.participants.map { it.owningKey }.toSet() + // TODO: We need a much faster way of finding our key in the transaction + val myKey = serviceHub.keyManagementService.filterMyKeys(participantKeys).single() + val stx = serviceHub.signInitialTransaction(baseTx, myKey) + return AbstractStateReplacementFlow.UpgradeTx(stx, participantKeys, myKey) } } diff --git a/core/src/main/kotlin/net/corda/flows/NotaryChangeFlow.kt b/core/src/main/kotlin/net/corda/flows/NotaryChangeFlow.kt index f99bcd2f3a..fff1fa3b3e 100644 --- a/core/src/main/kotlin/net/corda/flows/NotaryChangeFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/NotaryChangeFlow.kt @@ -7,6 +7,7 @@ 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 @@ -24,24 +25,25 @@ class NotaryChangeFlow( progressTracker: ProgressTracker = tracker()) : AbstractStateReplacementFlow.Instigator(originalState, newNotary, progressTracker) { - override fun assembleTx(): Pair> { + override fun assembleTx(): AbstractStateReplacementFlow.UpgradeTx { val state = originalState.state val tx = TransactionType.NotaryChange.Builder(originalState.state.notary) - val participants: Iterable - - if (state.encumbrance == null) { + val participants: Iterable = if (state.encumbrance == null) { val modifiedState = TransactionState(state.data, modification) tx.addInputState(originalState) tx.addOutputState(modifiedState) - participants = state.data.participants + state.data.participants } else { - participants = resolveEncumbrances(tx) + resolveEncumbrances(tx) } val stx = serviceHub.signInitialTransaction(tx) + val participantKeys = participants.map { it.owningKey } + // TODO: We need a much faster way of finding our key in the transaction + val myKey = serviceHub.keyManagementService.filterMyKeys(participantKeys).single() - return Pair(stx, participants) + return AbstractStateReplacementFlow.UpgradeTx(stx, participantKeys, myKey) } /** diff --git a/core/src/main/kotlin/net/corda/flows/TxKeyFlow.kt b/core/src/main/kotlin/net/corda/flows/TxKeyFlow.kt index ff50739ffc..c3f46e69eb 100644 --- a/core/src/main/kotlin/net/corda/flows/TxKeyFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/TxKeyFlow.kt @@ -5,13 +5,10 @@ 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. @@ -19,11 +16,11 @@ import java.security.cert.CertPath */ object TxKeyFlow { abstract class AbstractIdentityFlow(val otherSide: Party, val revocationEnabled: Boolean): FlowLogic() { - fun validateIdentity(untrustedIdentity: AnonymousIdentity): AnonymousIdentity { + fun validateIdentity(untrustedIdentity: AnonymisedIdentity): AnonymisedIdentity { val (certPath, theirCert, txIdentity) = untrustedIdentity if (theirCert.subject == otherSide.name) { serviceHub.identityService.registerAnonymousIdentity(txIdentity, otherSide, certPath) - return AnonymousIdentity(certPath, theirCert, txIdentity) + return AnonymisedIdentity(certPath, theirCert, txIdentity) } else throw IllegalStateException("Expected certificate subject to be ${otherSide.name} but found ${theirCert.subject}") } @@ -32,7 +29,7 @@ object TxKeyFlow { @StartableByRPC @InitiatingFlow class Requester(otherSide: Party, - override val progressTracker: ProgressTracker) : AbstractIdentityFlow>(otherSide, false) { + override val progressTracker: ProgressTracker) : AbstractIdentityFlow(otherSide, false) { constructor(otherSide: Party) : this(otherSide, tracker()) companion object { object AWAITING_KEY : ProgressTracker.Step("Awaiting key") @@ -41,14 +38,20 @@ object TxKeyFlow { } @Suspendable - override fun call(): Map { + override fun call(): TxIdentities { progressTracker.currentStep = AWAITING_KEY - val myIdentityFragment = serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, revocationEnabled) - val myIdentity = AnonymousIdentity(myIdentityFragment) - val theirIdentity = receive(otherSide).unwrap { validateIdentity(it) } - send(otherSide, myIdentity) - return mapOf(Pair(otherSide, myIdentity), - Pair(serviceHub.myInfo.legalIdentity, theirIdentity)) + val myIdentity = serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, revocationEnabled) + serviceHub.identityService.registerAnonymousIdentity(myIdentity.identity, serviceHub.myInfo.legalIdentity, myIdentity.certPath) + + // Special case that if we're both parties, a single identity is generated + return if (otherSide == serviceHub.myInfo.legalIdentity) { + TxIdentities(Pair(otherSide, myIdentity)) + } else { + val theirIdentity = receive(otherSide).unwrap { validateIdentity(it) } + send(otherSide, myIdentity) + TxIdentities(Pair(otherSide, myIdentity), + Pair(serviceHub.myInfo.legalIdentity, theirIdentity)) + } } } @@ -57,7 +60,7 @@ object TxKeyFlow { * counterparty and as the result from the flow. */ @InitiatedBy(Requester::class) - class Provider(otherSide: Party) : AbstractIdentityFlow>(otherSide, false) { + class Provider(otherSide: Party) : AbstractIdentityFlow(otherSide, false) { companion object { object SENDING_KEY : ProgressTracker.Step("Sending key") } @@ -65,25 +68,24 @@ object TxKeyFlow { override val progressTracker: ProgressTracker = ProgressTracker(SENDING_KEY) @Suspendable - override fun call(): Map { + override fun call(): TxIdentities { val revocationEnabled = false progressTracker.currentStep = SENDING_KEY - val myIdentityFragment = serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, revocationEnabled) - val myIdentity = AnonymousIdentity(myIdentityFragment) + val myIdentity = serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, revocationEnabled) send(otherSide, myIdentity) - val theirIdentity = receive(otherSide).unwrap { validateIdentity(it) } - return mapOf(Pair(otherSide, myIdentity), + val theirIdentity = receive(otherSide).unwrap { validateIdentity(it) } + return TxIdentities(Pair(otherSide, myIdentity), Pair(serviceHub.myInfo.legalIdentity, theirIdentity)) } } @CordaSerializable - data class AnonymousIdentity( - val certPath: CertPath, - val certificate: X509CertificateHolder, - val identity: AnonymousParty) { - constructor(myIdentity: Pair) : this(myIdentity.second, - myIdentity.first, - AnonymousParty(myIdentity.second.certificates.first().publicKey)) + data class TxIdentities(val identities: List>) { + constructor(vararg identities: Pair) : this(identities.toList()) + init { + require(identities.size == identities.map { it.first }.toSet().size) { "Identities must be unique: ${identities.map { it.first }}" } + } + fun forParty(party: Party): AnonymisedIdentity = identities.single { it.first == party }.second + fun toMap(): Map = this.identities.toMap() } } diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt index d0335f7c44..7160cf7c94 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -173,9 +173,11 @@ class ContractUpgradeFlowTest { @Test fun `upgrade Cash to v2`() { // Create some cash. - val result = a.services.startFlow(CashIssueFlow(Amount(1000, USD), OpaqueBytes.of(1), a.info.legalIdentity, notary)).resultFuture + val anonymous = false + val result = a.services.startFlow(CashIssueFlow(Amount(1000, USD), OpaqueBytes.of(1), a.info.legalIdentity, notary, anonymous)).resultFuture mockNet.runNetwork() - val stateAndRef = result.getOrThrow().tx.outRef(0) + val stx = result.getOrThrow().stx + val stateAndRef = stx.tx.outRef(0) val baseState = a.database.transaction { a.vault.unconsumedStates().single() } assertTrue(baseState.state.data is Cash.State, "Contract state is old version.") // Starts contract upgrade flow. diff --git a/core/src/test/kotlin/net/corda/flows/TxKeyFlowTests.kt b/core/src/test/kotlin/net/corda/flows/TxKeyFlowTests.kt index c439f5ee41..942ca591f9 100644 --- a/core/src/test/kotlin/net/corda/flows/TxKeyFlowTests.kt +++ b/core/src/test/kotlin/net/corda/flows/TxKeyFlowTests.kt @@ -40,7 +40,7 @@ class TxKeyFlowTests { val requesterFlow = aliceNode.services.startFlow(TxKeyFlow.Requester(bob)) // Get the results - val actual: Map = requesterFlow.resultFuture.getOrThrow() + val actual: Map = requesterFlow.resultFuture.getOrThrow().toMap() assertEquals(2, actual.size) // Verify that the generated anonymous identities do not match the well known identities val aliceAnonymousIdentity = actual[alice] ?: throw IllegalStateException() diff --git a/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt index af5ff519cd..061aaf5524 100644 --- a/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt +++ b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt @@ -67,7 +67,8 @@ class IntegrationTestingTutorial { i.DOLLARS, issueRef, bob.nodeInfo.legalIdentity, - notary.nodeInfo.notaryIdentity + notary.nodeInfo.notaryIdentity, + false // Not anonymised ).returnValue) } }.forEach(Thread::join) // Ensure the stack of futures is populated. @@ -90,7 +91,7 @@ class IntegrationTestingTutorial { // START 5 for (i in 1..10) { - bobProxy.startFlow(::CashPaymentFlow, i.DOLLARS, alice.nodeInfo.legalIdentity).returnValue.getOrThrow() + bobProxy.startFlow(::CashPaymentFlow, i.DOLLARS, alice.nodeInfo.legalIdentity, false).returnValue.getOrThrow() } aliceVaultUpdates.expectEvents { diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt index 33d75ce6ee..ce6ea008c6 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt @@ -13,7 +13,10 @@ import net.corda.core.flows.InitiatedBy import net.corda.core.flows.InitiatingFlow import net.corda.core.identity.Party import net.corda.core.node.ServiceHub +import net.corda.core.node.services.Vault +import net.corda.core.node.services.queryBy import net.corda.core.node.services.unconsumedStates +import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.unwrap @@ -39,14 +42,15 @@ private fun gatherOurInputs(serviceHub: ServiceHub, amountRequired: Amount>, notary: Party?): Pair>, Long> { // Collect cash type inputs - val cashStates = serviceHub.vaultService.unconsumedStates() + val queryCriteria = QueryCriteria.VaultQueryCriteria(Vault.StateStatus.UNCONSUMED, setOf(Cash.State::class.java)) + val cashStates = serviceHub.vaultQueryService.queryBy(queryCriteria).states // extract our identity for convenience - val ourIdentity = serviceHub.myInfo.legalIdentity + val ourKeys = serviceHub.keyManagementService.keys // Filter down to our own cash states with right currency and issuer val suitableCashStates = cashStates.filter { val state = it.state.data - (state.owner == ourIdentity) - && (state.amount.token == amountRequired.token) + // TODO: We may want to have the list of our states pre-cached somewhere for performance + (state.owner.owningKey in ourKeys) && (state.amount.token == amountRequired.token) } require(!suitableCashStates.isEmpty()) { "Insufficient funds" } var remaining = amountRequired.quantity @@ -132,9 +136,6 @@ class ForeignExchangeFlow(val tradeId: String, require(it.inputs.all { it.state.notary == notary }) { "notary of remote states must be same as for our states" } - require(it.inputs.all { it.state.data.owner == remoteRequestWithNotary.owner }) { - "The inputs are not owned by the correct counterparty" - } require(it.inputs.all { it.state.data.amount.token == remoteRequestWithNotary.amount.token }) { "Inputs not of the correct currency" } @@ -200,7 +201,7 @@ class ForeignExchangeFlow(val tradeId: String, // We have already validated their response and trust our own data // so we can sign. Note the returned SignedTransaction is still not fully signed // and would not pass full verification yet. - return serviceHub.signInitialTransaction(builder) + return serviceHub.signInitialTransaction(builder, ourSigners.single()) } // DOCEND 3 } @@ -234,10 +235,11 @@ class ForeignExchangeRemoteFlow(val source: Party) : FlowLogic() { val ourResponse = prepareOurInputsAndOutputs(serviceHub, request) // Send back our proposed states and await the full transaction to verify + val ourKey = serviceHub.keyManagementService.filterMyKeys(ourResponse.inputs.flatMap { it.state.data.participants }.map { it.owningKey }).single() val proposedTrade = sendAndReceive(source, ourResponse).unwrap { val wtx = it.tx // check all signatures are present except our own and the notary - it.verifySignatures(serviceHub.myInfo.legalIdentity.owningKey, wtx.notary!!.owningKey) + it.verifySignatures(ourKey, wtx.notary!!.owningKey) // We need to fetch their complete input states and dependencies so that verify can operate checkDependencies(it) @@ -251,7 +253,7 @@ class ForeignExchangeRemoteFlow(val source: Party) : FlowLogic() { } // assuming we have completed state and business level validation we can sign the trade - val ourSignature = serviceHub.createSignature(proposedTrade) + val ourSignature = serviceHub.createSignature(proposedTrade, ourKey) // send the other side our signature. send(source, ourSignature) diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt index b9b7abd4fa..5f865dd02d 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt @@ -48,7 +48,8 @@ class FxTransactionBuildTutorialTest { val flowHandle1 = nodeA.services.startFlow(CashIssueFlow(DOLLARS(1000), OpaqueBytes.of(0x01), nodeA.info.legalIdentity, - notaryNode.info.notaryIdentity)) + notaryNode.info.notaryIdentity, + false)) // Wait for the flow to stop and print flowHandle1.resultFuture.getOrThrow() printBalances() @@ -57,7 +58,8 @@ class FxTransactionBuildTutorialTest { val flowHandle2 = nodeB.services.startFlow(CashIssueFlow(POUNDS(1000), OpaqueBytes.of(0x01), nodeB.info.legalIdentity, - notaryNode.info.notaryIdentity)) + notaryNode.info.notaryIdentity, + false)) // Wait for flow to come to an end and print flowHandle2.resultFuture.getOrThrow() printBalances() diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt b/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt index a71f948fa9..1af1959760 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt @@ -429,7 +429,7 @@ class Obligation

: Contract { /** * Generate a transaction performing close-out netting of two or more states. * - * @param signer the party who will sign the transaction. Must be one of the obligor or beneficiary. + * @param signer the party which will sign the transaction. Must be one of the obligor or beneficiary. * @param states two or more states, which must be compatible for bilateral netting (same issuance definitions, * and same parties involved). */ @@ -458,7 +458,7 @@ class Obligation

: Contract { * @param amountIssued the amount to be exited, represented as a quantity of issued currency. * @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is * the responsibility of the caller to check that they do not exit funds held by others. - * @return the public keys who must sign the transaction for it to be valid. + * @return the public keys which must sign the transaction for it to be valid. */ @Suppress("unused") fun generateExit(tx: TransactionBuilder, amountIssued: Amount>>, diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/OnLedgerAsset.kt b/finance/src/main/kotlin/net/corda/contracts/asset/OnLedgerAsset.kt index a6d3bd4185..383ddab54d 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/OnLedgerAsset.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/OnLedgerAsset.kt @@ -207,13 +207,15 @@ abstract class OnLedgerAsset> : C @JvmStatic fun , T: Any> generateIssue(tx: TransactionBuilder, transactionState: TransactionState, - issueCommand: CommandData) { + issueCommand: CommandData): Set { check(tx.inputStates().isEmpty()) check(tx.outputStates().map { it.data }.filterIsInstance(transactionState.javaClass).isEmpty()) require(transactionState.data.amount.quantity > 0) val at = transactionState.data.amount.token.issuer + val commandSigner = at.party.owningKey tx.addOutputState(transactionState) - tx.addCommand(issueCommand, at.party.owningKey) + tx.addCommand(issueCommand, commandSigner) + return setOf(commandSigner) } } diff --git a/finance/src/main/kotlin/net/corda/flows/AbstractCashFlow.kt b/finance/src/main/kotlin/net/corda/flows/AbstractCashFlow.kt index 7b32fd8afb..a744a9cdee 100644 --- a/finance/src/main/kotlin/net/corda/flows/AbstractCashFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/AbstractCashFlow.kt @@ -3,30 +3,44 @@ package net.corda.flows import co.paralleluniverse.fibers.Suspendable import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic +import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party +import net.corda.core.identity.PartyAndCertificate +import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.ProgressTracker /** * Initiates a flow that produces an Issue/Move or Exit Cash transaction. */ -abstract class AbstractCashFlow(override val progressTracker: ProgressTracker) : FlowLogic() { +abstract class AbstractCashFlow(override val progressTracker: ProgressTracker) : FlowLogic() { companion object { + object GENERATING_ID : ProgressTracker.Step("Generating anonymous identities") object GENERATING_TX : ProgressTracker.Step("Generating transaction") object SIGNING_TX : ProgressTracker.Step("Signing transaction") object FINALISING_TX : ProgressTracker.Step("Finalising transaction") - fun tracker() = ProgressTracker(GENERATING_TX, SIGNING_TX, FINALISING_TX) + fun tracker() = ProgressTracker(GENERATING_ID, GENERATING_TX, SIGNING_TX, FINALISING_TX) } @Suspendable - internal fun finaliseTx(participants: Set, tx: SignedTransaction, message: String) { + protected fun finaliseTx(participants: Set, tx: SignedTransaction, message: String) { try { subFlow(FinalityFlow(tx, participants)) } catch (e: NotaryException) { throw CashException(message, e) } } + + /** + * Combined signed transaction and identity lookup map, which is the resulting data from regular cash flows. + * Specialised flows for unit tests differ from this. + * + * @param stx the signed transaction. + * @param identities a mapping from the original identities of the parties to the anonymised equivalents. + */ + @CordaSerializable + data class Result(val stx: SignedTransaction, val identities: TxKeyFlow.TxIdentities) } class CashException(message: String, cause: Throwable) : FlowException(message, cause) \ No newline at end of file diff --git a/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt b/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt index 7a9e441668..7477364082 100644 --- a/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt @@ -9,7 +9,6 @@ import net.corda.core.contracts.issuedBy import net.corda.core.flows.StartableByRPC import net.corda.core.identity.Party import net.corda.core.serialization.OpaqueBytes -import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker import java.util.* @@ -22,16 +21,20 @@ import java.util.* * issuer. */ @StartableByRPC -class CashExitFlow(val amount: Amount, val issueRef: OpaqueBytes, progressTracker: ProgressTracker) : AbstractCashFlow(progressTracker) { +class CashExitFlow(val amount: Amount, val issueRef: OpaqueBytes, progressTracker: ProgressTracker) : AbstractCashFlow(progressTracker) { constructor(amount: Amount, issueRef: OpaqueBytes) : this(amount, issueRef, tracker()) companion object { fun tracker() = ProgressTracker(GENERATING_TX, SIGNING_TX, FINALISING_TX) } + /** + * @return the signed transaction, and a mapping of parties to new anonymous identities generated + * (for this flow this map is always empty). + */ @Suspendable @Throws(CashException::class) - override fun call(): SignedTransaction { + override fun call(): AbstractCashFlow.Result { progressTracker.currentStep = GENERATING_TX val builder: TransactionBuilder = TransactionType.General.Builder(notary = null as Party?) val issuer = serviceHub.myInfo.legalIdentity.ref(issueRef) @@ -67,6 +70,6 @@ class CashExitFlow(val amount: Amount, val issueRef: OpaqueBytes, prog // Commit the transaction progressTracker.currentStep = FINALISING_TX finaliseTx(participants, tx, "Unable to notarise exit") - return tx + return Result(tx, TxKeyFlow.TxIdentities()) } } diff --git a/finance/src/main/kotlin/net/corda/flows/CashFlowCommand.kt b/finance/src/main/kotlin/net/corda/flows/CashFlowCommand.kt index 69fcdd91b9..30012b6b9e 100644 --- a/finance/src/main/kotlin/net/corda/flows/CashFlowCommand.kt +++ b/finance/src/main/kotlin/net/corda/flows/CashFlowCommand.kt @@ -6,14 +6,13 @@ import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.FlowHandle import net.corda.core.messaging.startFlow import net.corda.core.serialization.OpaqueBytes -import net.corda.core.transactions.SignedTransaction import java.util.* /** * A command to initiate the cash flow with. */ sealed class CashFlowCommand { - abstract fun startFlow(proxy: CordaRPCOps): FlowHandle + abstract fun startFlow(proxy: CordaRPCOps): FlowHandle /** * A command to initiate the Cash flow with. @@ -21,8 +20,9 @@ sealed class CashFlowCommand { data class IssueCash(val amount: Amount, val issueRef: OpaqueBytes, val recipient: Party, - val notary: Party) : CashFlowCommand() { - override fun startFlow(proxy: CordaRPCOps) = proxy.startFlow(::CashIssueFlow, amount, issueRef, recipient, notary) + val notary: Party, + val anonymous: Boolean) : CashFlowCommand() { + override fun startFlow(proxy: CordaRPCOps) = proxy.startFlow(::CashIssueFlow, amount, issueRef, recipient, notary, anonymous) } /** @@ -31,8 +31,9 @@ sealed class CashFlowCommand { * @param amount the amount of currency to issue on to the ledger. * @param recipient the party to issue the cash to. */ - data class PayCash(val amount: Amount, val recipient: Party, val issuerConstraint: Party? = null) : CashFlowCommand() { - override fun startFlow(proxy: CordaRPCOps) = proxy.startFlow(::CashPaymentFlow, amount, recipient) + data class PayCash(val amount: Amount, val recipient: Party, val issuerConstraint: Party? = null, + val anonymous: Boolean) : CashFlowCommand() { + override fun startFlow(proxy: CordaRPCOps) = proxy.startFlow(::CashPaymentFlow, amount, recipient, anonymous) } /** diff --git a/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt b/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt index 479e1a1d6d..7fa563e6d8 100644 --- a/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt @@ -5,10 +5,9 @@ import net.corda.contracts.asset.Cash import net.corda.core.contracts.Amount import net.corda.core.contracts.TransactionType import net.corda.core.contracts.issuedBy -import net.corda.core.identity.Party import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.Party import net.corda.core.serialization.OpaqueBytes -import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker import java.util.* @@ -26,23 +25,39 @@ class CashIssueFlow(val amount: Amount, val issueRef: OpaqueBytes, val recipient: Party, val notary: Party, - progressTracker: ProgressTracker) : AbstractCashFlow(progressTracker) { + val anonymous: Boolean, + progressTracker: ProgressTracker) : AbstractCashFlow(progressTracker) { constructor(amount: Amount, issueRef: OpaqueBytes, recipient: Party, - notary: Party) : this(amount, issueRef, recipient, notary, tracker()) + notary: Party) : this(amount, issueRef, recipient, notary, true, tracker()) + constructor(amount: Amount, + issueRef: OpaqueBytes, + recipient: Party, + notary: Party, + anonymous: Boolean) : this(amount, issueRef, recipient, notary, anonymous, tracker()) @Suspendable - override fun call(): SignedTransaction { + override fun call(): AbstractCashFlow.Result { + progressTracker.currentStep = GENERATING_ID + val txIdentities = if (anonymous) { + subFlow(TxKeyFlow.Requester(recipient)) + } else { + TxKeyFlow.TxIdentities(emptyList()) + } + val anonymousRecipient = if (anonymous) { + txIdentities.forParty(recipient).identity + } else { + recipient + } progressTracker.currentStep = GENERATING_TX val builder: TransactionBuilder = TransactionType.General.Builder(notary = notary) val issuer = serviceHub.myInfo.legalIdentity.ref(issueRef) - // TODO: Get a transaction key, don't just re-use the owning key - Cash().generateIssue(builder, amount.issuedBy(issuer), recipient, notary) + val signers = Cash().generateIssue(builder, amount.issuedBy(issuer), anonymousRecipient, notary) progressTracker.currentStep = SIGNING_TX - val tx = serviceHub.signInitialTransaction(builder) + val tx = serviceHub.signInitialTransaction(builder, signers) progressTracker.currentStep = FINALISING_TX subFlow(FinalityFlow(tx)) - return tx + return Result(tx, txIdentities) } } diff --git a/finance/src/main/kotlin/net/corda/flows/CashPaymentFlow.kt b/finance/src/main/kotlin/net/corda/flows/CashPaymentFlow.kt index b7642d24c1..2bc40307c6 100644 --- a/finance/src/main/kotlin/net/corda/flows/CashPaymentFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/CashPaymentFlow.kt @@ -6,7 +6,6 @@ import net.corda.core.contracts.InsufficientBalanceException import net.corda.core.contracts.TransactionType import net.corda.core.flows.StartableByRPC 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.util.* @@ -17,18 +16,34 @@ import java.util.* * @param amount the amount of a currency to pay to the recipient. * @param recipient the party to pay the currency to. * @param issuerConstraint if specified, the payment will be made using only cash issued by the given parties. + * @param anonymous whether to anonymous the recipient party. Should be true for normal usage, but may be false + * for testing purposes. */ @StartableByRPC open class CashPaymentFlow( val amount: Amount, val recipient: Party, + val anonymous: Boolean, progressTracker: ProgressTracker, - val issuerConstraint: Set? = null) : AbstractCashFlow(progressTracker) { + val issuerConstraint: Set? = null) : AbstractCashFlow(progressTracker) { /** A straightforward constructor that constructs spends using cash states of any issuer. */ - constructor(amount: Amount, recipient: Party) : this(amount, recipient, tracker()) + constructor(amount: Amount, recipient: Party) : this(amount, recipient, true, tracker()) + /** A straightforward constructor that constructs spends using cash states of any issuer. */ + constructor(amount: Amount, recipient: Party, anonymous: Boolean) : this(amount, recipient, anonymous, tracker()) @Suspendable - override fun call(): SignedTransaction { + override fun call(): AbstractCashFlow.Result { + progressTracker.currentStep = GENERATING_ID + val txIdentities = if (anonymous) { + subFlow(TxKeyFlow.Requester(recipient)) + } else { + TxKeyFlow.TxIdentities(emptyList()) + } + val anonymousRecipient = if (anonymous) { + txIdentities.forParty(recipient).identity + } else { + recipient + } progressTracker.currentStep = GENERATING_TX val builder: TransactionBuilder = TransactionType.General.Builder(null as Party?) // TODO: Have some way of restricting this to states the caller controls @@ -36,8 +51,7 @@ open class CashPaymentFlow( serviceHub.vaultService.generateSpend( builder, amount, - // TODO: Get a transaction key, don't just re-use the owning key - recipient, + anonymousRecipient, issuerConstraint) } catch (e: InsufficientBalanceException) { throw CashException("Insufficient cash for spend: ${e.message}", e) @@ -48,6 +62,6 @@ open class CashPaymentFlow( progressTracker.currentStep = FINALISING_TX finaliseTx(setOf(recipient), tx, "Unable to notarise spend") - return tx + return Result(tx, txIdentities) } } \ No newline at end of file diff --git a/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt b/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt index 50bfe72327..af01f2c069 100644 --- a/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt @@ -1,8 +1,10 @@ package net.corda.flows import co.paralleluniverse.fibers.Suspendable +import net.corda.contracts.asset.Cash import net.corda.core.contracts.* import net.corda.core.flows.* +import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.OpaqueBytes @@ -20,21 +22,45 @@ import java.util.* */ object IssuerFlow { @CordaSerializable - data class IssuanceRequestState(val amount: Amount, val issueToParty: Party, val issuerPartyRef: OpaqueBytes) + data class IssuanceRequestState(val amount: Amount, + val issueToParty: Party, + val issuerPartyRef: OpaqueBytes, + val anonymous: Boolean) /** * IssuanceRequester should be used by a client to ask a remote node to issue some [FungibleAsset] with the given details. * Returns the transaction created by the Issuer to move the cash to the Requester. + * + * @param anonymous true if the issued asset should be sent to a new confidential identity, false to send it to the + * well known identity (generally this is only used in testing). */ @InitiatingFlow @StartableByRPC - class IssuanceRequester(val amount: Amount, val issueToParty: Party, val issueToPartyRef: OpaqueBytes, - val issuerBankParty: Party) : FlowLogic() { + class IssuanceRequester(val amount: Amount, + val issueToParty: Party, + val issueToPartyRef: OpaqueBytes, + val issuerBankParty: Party, + val anonymous: Boolean) : FlowLogic() { @Suspendable @Throws(CashException::class) - override fun call(): SignedTransaction { - val issueRequest = IssuanceRequestState(amount, issueToParty, issueToPartyRef) - return sendAndReceive(issuerBankParty, issueRequest).unwrap { it } + override fun call(): AbstractCashFlow.Result { + val issueRequest = IssuanceRequestState(amount, issueToParty, issueToPartyRef, anonymous) + return sendAndReceive(issuerBankParty, issueRequest).unwrap { res -> + val tx = res.stx.tx + val recipient = if (anonymous) { + res.identities.forParty(issueToParty).identity + } else { + issueToParty + } + val expectedAmount = Amount(amount.quantity, Issued(issuerBankParty.ref(issueToPartyRef), amount.token)) + val cashOutputs = tx.outputs + .map { it.data} + .filterIsInstance() + .filter { state -> state.owner == recipient } + require(cashOutputs.size == 1) { "Require a single cash output paying $recipient, found ${tx.outputs}" } + require(cashOutputs.single().amount == expectedAmount) { "Require payment of $expectedAmount"} + res + } } } @@ -66,22 +92,23 @@ object IssuerFlow { it } // TODO: parse request to determine Asset to issue - val txn = issueCashTo(issueRequest.amount, issueRequest.issueToParty, issueRequest.issuerPartyRef) + val txn = issueCashTo(issueRequest.amount, issueRequest.issueToParty, issueRequest.issuerPartyRef, issueRequest.anonymous) progressTracker.currentStep = SENDING_CONFIRM send(otherParty, txn) - return txn + return txn.stx } @Suspendable private fun issueCashTo(amount: Amount, issueTo: Party, - issuerPartyRef: OpaqueBytes): SignedTransaction { + issuerPartyRef: OpaqueBytes, + anonymous: Boolean): AbstractCashFlow.Result { // TODO: pass notary in as request parameter val notaryParty = serviceHub.networkMapCache.notaryNodes[0].notaryIdentity // invoke Cash subflow to issue Asset progressTracker.currentStep = ISSUING - val bankOfCordaParty = serviceHub.myInfo.legalIdentity - val issueCashFlow = CashIssueFlow(amount, issuerPartyRef, bankOfCordaParty, notaryParty) + val issueRecipient = serviceHub.myInfo.legalIdentity + val issueCashFlow = CashIssueFlow(amount, issuerPartyRef, issueRecipient, notaryParty, anonymous = false) val issueTx = subFlow(issueCashFlow) // NOTE: issueCashFlow performs a Broadcast (which stores a local copy of the txn to the ledger) // short-circuit when issuing to self @@ -89,7 +116,7 @@ object IssuerFlow { return issueTx // now invoke Cash subflow to Move issued assetType to issue requester progressTracker.currentStep = TRANSFERRING - val moveCashFlow = CashPaymentFlow(amount, issueTo) + val moveCashFlow = CashPaymentFlow(amount, issueTo, anonymous) val moveTx = subFlow(moveCashFlow) // NOTE: CashFlow PayCash calls FinalityFlow which performs a Broadcast (which stores a local copy of the txn to the ledger) return moveTx diff --git a/finance/src/test/kotlin/net/corda/flows/CashExitFlowTests.kt b/finance/src/test/kotlin/net/corda/flows/CashExitFlowTests.kt index 9a09229162..bb9231fe42 100644 --- a/finance/src/test/kotlin/net/corda/flows/CashExitFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/flows/CashExitFlowTests.kt @@ -3,8 +3,8 @@ package net.corda.flows import net.corda.contracts.asset.Cash import net.corda.core.contracts.DOLLARS import net.corda.core.contracts.`issued by` -import net.corda.core.identity.Party import net.corda.core.getOrThrow +import net.corda.core.identity.Party import net.corda.core.serialization.OpaqueBytes import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin import net.corda.testing.node.MockNetwork @@ -51,7 +51,7 @@ class CashExitFlowTests { val future = bankOfCordaNode.services.startFlow(CashExitFlow(exitAmount, ref)).resultFuture mockNet.runNetwork() - val exitTx = future.getOrThrow().tx + val exitTx = future.getOrThrow().stx.tx val expected = (initialBalance - exitAmount).`issued by`(bankOfCorda.ref(ref)) assertEquals(1, exitTx.inputs.size) assertEquals(1, exitTx.outputs.size) diff --git a/finance/src/test/kotlin/net/corda/flows/CashIssueFlowTests.kt b/finance/src/test/kotlin/net/corda/flows/CashIssueFlowTests.kt index 5f81c03e40..78abdb4bf0 100644 --- a/finance/src/test/kotlin/net/corda/flows/CashIssueFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/flows/CashIssueFlowTests.kt @@ -3,8 +3,8 @@ package net.corda.flows import net.corda.contracts.asset.Cash import net.corda.core.contracts.DOLLARS import net.corda.core.contracts.`issued by` -import net.corda.core.identity.Party import net.corda.core.getOrThrow +import net.corda.core.identity.Party import net.corda.core.serialization.OpaqueBytes import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin import net.corda.testing.node.MockNetwork @@ -46,7 +46,7 @@ class CashIssueFlowTests { bankOfCorda, notary)).resultFuture mockNet.runNetwork() - val issueTx = future.getOrThrow() + val issueTx = future.getOrThrow().stx val output = issueTx.tx.outputs.single().data as Cash.State assertEquals(expected.`issued by`(bankOfCorda.ref(ref)), output.amount) } diff --git a/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt b/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt index 58779cf1b7..eada1d6187 100644 --- a/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt @@ -32,7 +32,9 @@ class CashPaymentFlowTests { notary = notaryNode.info.notaryIdentity bankOfCorda = bankOfCordaNode.info.legalIdentity - mockNet.runNetwork() + notaryNode.registerInitiatedFlow(TxKeyFlow.Provider::class.java) + notaryNode.identity.registerIdentity(bankOfCordaNode.info.legalIdentityAndCert) + bankOfCordaNode.identity.registerIdentity(notaryNode.info.legalIdentityAndCert) val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, ref, bankOfCorda, notary)).resultFuture @@ -53,11 +55,11 @@ class CashPaymentFlowTests { val future = bankOfCordaNode.services.startFlow(CashPaymentFlow(expectedPayment, payTo)).resultFuture mockNet.runNetwork() - val paymentTx = future.getOrThrow() + val (paymentTx, identities) = future.getOrThrow() val states = paymentTx.tx.outputs.map { it.data }.filterIsInstance() - val ourState = states.single { it.owner.owningKey != payTo.owningKey } - val paymentState = states.single { it.owner.owningKey == payTo.owningKey } - assertEquals(expectedChange.`issued by`(bankOfCorda.ref(ref)), ourState.amount) + val paymentState: Cash.State = states.single { it.owner == identities.forParty(payTo).identity } + val changeState: Cash.State = states.single { it != paymentState } + assertEquals(expectedChange.`issued by`(bankOfCorda.ref(ref)), changeState.amount) assertEquals(expectedPayment.`issued by`(bankOfCorda.ref(ref)), paymentState.amount) } diff --git a/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt b/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt index e69f0947ee..32217fde9a 100644 --- a/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt +++ b/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt @@ -39,6 +39,12 @@ class IssuerFlowTest { notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) bankOfCordaNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name) bankClientNode = mockNet.createPartyNode(notaryNode.network.myAddress, MEGA_CORP.name) + val nodes = listOf(notaryNode, bankOfCordaNode, bankClientNode) + + nodes.forEach { node -> + nodes.map { it.info.legalIdentityAndCert }.forEach(node.services.identityService::registerIdentity) + node.registerInitiatedFlow(TxKeyFlow.Provider::class.java) + } } @After @@ -51,7 +57,7 @@ class IssuerFlowTest { // using default IssueTo Party Reference val (issuer, issuerResult) = runIssuerAndIssueRequester(bankOfCordaNode, bankClientNode, 1000000.DOLLARS, bankClientNode.info.legalIdentity, OpaqueBytes.of(123)) - assertEquals(issuerResult.get(), issuer.get().resultFuture.get()) + assertEquals(issuerResult.get().stx, issuer.get().resultFuture.get()) // try to issue an amount of a restricted currency assertFailsWith { @@ -65,7 +71,7 @@ class IssuerFlowTest { // using default IssueTo Party Reference val (issuer, issuerResult) = runIssuerAndIssueRequester(bankOfCordaNode, bankOfCordaNode, 1000000.DOLLARS, bankOfCordaNode.info.legalIdentity, OpaqueBytes.of(123)) - assertEquals(issuerResult.get(), issuer.get().resultFuture.get()) + assertEquals(issuerResult.get().stx, issuer.get().resultFuture.get()) } @Test @@ -78,7 +84,7 @@ class IssuerFlowTest { bankClientNode.info.legalIdentity, OpaqueBytes.of(123)) } handles.forEach { - require(it.issueRequestResult.get() is SignedTransaction) + require(it.issueRequestResult.get().stx is SignedTransaction) } } @@ -91,7 +97,8 @@ class IssuerFlowTest { val issuerFlows: Observable = issuerNode.registerInitiatedFlow(IssuerFlow.Issuer::class.java) val firstIssuerFiber = issuerFlows.toFuture().map { it.stateMachine } - val issueRequest = IssuanceRequester(amount, party, issueToPartyAndRef.reference, issuerNode.info.legalIdentity) + val issueRequest = IssuanceRequester(amount, party, issueToPartyAndRef.reference, issuerNode.info.legalIdentity, + anonymous = false) val issueRequestResultFuture = issueToNode.services.startFlow(issueRequest).resultFuture return IssuerFlowTest.RunResult(firstIssuerFiber, issueRequestResultFuture) @@ -99,6 +106,6 @@ class IssuerFlowTest { private data class RunResult( val issuer: ListenableFuture>, - val issueRequestResult: ListenableFuture + val issueRequestResult: ListenableFuture ) } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index ac397c89d8..1f28e00a0b 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -260,6 +260,8 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, rpcFlows = emptyList() } + // TODO: Investigate having class path scanning find this flow + registerInitiatedFlow(TxKeyFlow.Provider::class.java) // TODO Remove this once the cash stuff is in its own CorDapp registerInitiatedFlow(IssuerFlow.Issuer::class.java) @@ -459,7 +461,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val storageServices = initialiseStorageService(configuration.baseDirectory) storage = storageServices.first checkpointStorage = storageServices.second - netMapCache = InMemoryNetworkMapCache() + netMapCache = InMemoryNetworkMapCache(services) network = makeMessagingService() schemas = makeSchemaService() vault = makeVaultService(configuration.dataSourceProperties) diff --git a/node/src/main/kotlin/net/corda/node/services/keys/E2ETestKeyManagementService.kt b/node/src/main/kotlin/net/corda/node/services/keys/E2ETestKeyManagementService.kt index fd7074833f..0222ba6fa5 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/E2ETestKeyManagementService.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/E2ETestKeyManagementService.kt @@ -5,11 +5,11 @@ import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.generateKeyPair import net.corda.core.crypto.keys import net.corda.core.crypto.sign -import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.services.IdentityService import net.corda.core.node.services.KeyManagementService import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.flows.AnonymisedIdentity import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.operator.ContentSigner import java.security.KeyPair @@ -58,7 +58,7 @@ class E2ETestKeyManagementService(val identityService: IdentityService, return keyPair.public } - override fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): Pair { + override fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): AnonymisedIdentity { return freshCertificate(identityService, freshKey(), identity, getSigner(identity.owningKey), revocationEnabled) } @@ -71,6 +71,10 @@ class E2ETestKeyManagementService(val identityService: IdentityService, } } + override fun filterMyKeys(candidateKeys: Iterable): Iterable { + return mutex.locked { candidateKeys.filter { it in this.keys } } + } + override fun sign(bytes: ByteArray, publicKey: PublicKey): DigitalSignature.WithKey { val keyPair = getSigningKeyPair(publicKey) val signature = keyPair.sign(bytes) diff --git a/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt b/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt index 7f4dff1c5e..481e9e8246 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt @@ -4,6 +4,7 @@ import net.corda.core.crypto.* import net.corda.core.identity.AnonymousParty import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.services.IdentityService +import net.corda.flows.AnonymisedIdentity import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.operator.ContentSigner import java.security.KeyPair @@ -30,7 +31,7 @@ fun freshCertificate(identityService: IdentityService, subjectPublicKey: PublicKey, issuer: PartyAndCertificate, issuerSigner: ContentSigner, - revocationEnabled: Boolean = false): Pair { + revocationEnabled: Boolean = false): AnonymisedIdentity { val issuerCertificate = issuer.certificate val window = X509Utilities.getCertificateValidityWindow(Duration.ZERO, Duration.ofDays(10 * 365), issuerCertificate) val ourCertificate = Crypto.createCertificate(CertificateType.IDENTITY, issuerCertificate.subject, issuerSigner, issuer.name, subjectPublicKey, window) @@ -39,7 +40,7 @@ fun freshCertificate(identityService: IdentityService, identityService.registerAnonymousIdentity(AnonymousParty(subjectPublicKey), issuer.party, ourCertPath) - return Pair(issuerCertificate, ourCertPath) + return AnonymisedIdentity(ourCertPath, issuerCertificate, subjectPublicKey) } fun getSigner(issuerKeyPair: KeyPair): ContentSigner { diff --git a/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt b/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt index c1f2c692a5..1a9b5be81e 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt @@ -9,6 +9,7 @@ import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.services.IdentityService import net.corda.core.node.services.KeyManagementService import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.flows.AnonymisedIdentity import net.corda.node.utilities.* import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.operator.ContentSigner @@ -60,6 +61,10 @@ class PersistentKeyManagementService(val identityService: IdentityService, override val keys: Set get() = mutex.locked { keys.keys } + override fun filterMyKeys(candidateKeys: Iterable): Iterable { + return mutex.locked { candidateKeys.filter { it in this.keys } } + } + override fun freshKey(): PublicKey { val keyPair = generateKeyPair() mutex.locked { @@ -68,7 +73,7 @@ class PersistentKeyManagementService(val identityService: IdentityService, return keyPair.public } - override fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): Pair { + override fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): AnonymisedIdentity { return freshCertificate(identityService, freshKey(), identity, getSigner(identity.owningKey), revocationEnabled) } diff --git a/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt b/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt index ab5bbeee03..0d3cd6e818 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt @@ -4,12 +4,15 @@ import com.google.common.annotations.VisibleForTesting import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.SettableFuture import net.corda.core.bufferUntilSubscribed +import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.map import net.corda.core.messaging.DataFeed import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.NodeInfo +import net.corda.core.node.ServiceHub import net.corda.core.node.services.DEFAULT_SESSION_ID +import net.corda.core.node.services.IdentityService import net.corda.core.node.services.NetworkMapCache.MapChange import net.corda.core.node.services.PartyInfo import net.corda.core.serialization.SingletonSerializeAsToken @@ -35,9 +38,13 @@ import javax.annotation.concurrent.ThreadSafe /** * Extremely simple in-memory cache of the network map. + * + * @param serviceHub an optional service hub from which we'll take the identity service. We take a service hub rather + * than the identity service directly, as this avoids problems with service start sequence (network map cache + * and identity services depend on each other). Should always be provided except for unit test cases. */ @ThreadSafe -open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCacheInternal { +open class InMemoryNetworkMapCache(private val serviceHub: ServiceHub?) : SingletonSerializeAsToken(), NetworkMapCacheInternal { companion object { val logger = loggerFor() } @@ -71,6 +78,17 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach } override fun getNodeByLegalIdentityKey(identityKey: PublicKey): NodeInfo? = registeredNodes[identityKey] + override fun getNodeByLegalIdentity(party: AbstractParty): NodeInfo? { + val wellKnownParty = if (serviceHub != null) { + serviceHub.identityService.partyFromAnonymous(party) + } else { + party + } + + return wellKnownParty?.let { + getNodeByLegalIdentityKey(it.owningKey) + } + } override fun track(): DataFeed, MapChange> { synchronized(_changed) { diff --git a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt index 37629331f6..7dc7cbfd09 100644 --- a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt @@ -7,6 +7,7 @@ import net.corda.core.crypto.isFulfilledBy import net.corda.core.crypto.keys import net.corda.core.flows.FlowLogic import net.corda.core.flows.StateMachineRunId +import net.corda.core.getOrThrow import net.corda.core.messaging.* import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.Vault @@ -86,18 +87,11 @@ class CordaRPCOpsImplTest { } // Tell the monitoring service node to issue some cash + val anonymous = false val recipient = aliceNode.info.legalIdentity - rpc.startFlow(::CashIssueFlow, Amount(quantity, GBP), ref, recipient, notaryNode.info.notaryIdentity) + val result = rpc.startFlow(::CashIssueFlow, Amount(quantity, GBP), ref, recipient, notaryNode.info.notaryIdentity, anonymous) mockNet.runNetwork() - val expectedState = Cash.State(Amount(quantity, - Issued(aliceNode.info.legalIdentity.ref(ref), GBP)), - recipient) - - // Query vault via RPC - val cash = rpc.vaultQueryBy() - assertEquals(expectedState, cash.states.first().state.data) - var issueSmId: StateMachineRunId? = null stateMachineUpdates.expectEvents { sequence( @@ -111,11 +105,14 @@ class CordaRPCOpsImplTest { ) } - transactions.expectEvents { - expect { tx -> - assertEquals(expectedState, tx.tx.outputs.single().data) - } - } + val tx = result.returnValue.getOrThrow() + val expectedState = Cash.State(Amount(quantity, + Issued(aliceNode.info.legalIdentity.ref(ref), GBP)), + recipient) + + // Query vault via RPC + val cash = rpc.vaultQueryBy() + assertEquals(expectedState, cash.states.first().state.data) // TODO: deprecated vaultUpdates.expectEvents { @@ -135,22 +132,24 @@ class CordaRPCOpsImplTest { @Test fun `issue and move`() { - rpc.startFlow(::CashIssueFlow, + val anonymous = false + val result = rpc.startFlow(::CashIssueFlow, Amount(100, USD), OpaqueBytes(ByteArray(1, { 1 })), aliceNode.info.legalIdentity, - notaryNode.info.notaryIdentity + notaryNode.info.notaryIdentity, + false ) mockNet.runNetwork() - rpc.startFlow(::CashPaymentFlow, Amount(100, USD), aliceNode.info.legalIdentity) + rpc.startFlow(::CashPaymentFlow, Amount(100, USD), aliceNode.info.legalIdentity, anonymous) mockNet.runNetwork() var issueSmId: StateMachineRunId? = null var moveSmId: StateMachineRunId? = null - stateMachineUpdates.expectEvents { + stateMachineUpdates.expectEvents() { sequence( // ISSUE expect { add: StateMachineUpdate.Added -> @@ -169,6 +168,7 @@ class CordaRPCOpsImplTest { ) } + val tx = result.returnValue.getOrThrow() transactions.expectEvents { sequence( // ISSUE @@ -233,7 +233,8 @@ class CordaRPCOpsImplTest { Amount(100, USD), OpaqueBytes(ByteArray(1, { 1 })), aliceNode.info.legalIdentity, - notaryNode.info.notaryIdentity + notaryNode.info.notaryIdentity, + false ) } } diff --git a/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt b/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt index 5b3194df6b..b7653f2a58 100644 --- a/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt +++ b/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt @@ -27,7 +27,7 @@ open class MockServiceHubInternal( val network: MessagingService? = null, val identity: IdentityService? = MOCK_IDENTITY_SERVICE, val storage: TxWritableStorageService? = MockStorageService(), - val mapCache: NetworkMapCacheInternal? = MockNetworkMapCache(), + val mapCache: NetworkMapCacheInternal? = null, val scheduler: SchedulerService? = null, val overrideClock: Clock? = NodeClock(), val schemas: SchemaService? = NodeSchemaService(), @@ -46,7 +46,7 @@ open class MockServiceHubInternal( override val networkService: MessagingService get() = network ?: throw UnsupportedOperationException() override val networkMapCache: NetworkMapCacheInternal - get() = mapCache ?: throw UnsupportedOperationException() + get() = mapCache ?: MockNetworkMapCache(this) override val storageService: StorageService get() = storage ?: throw UnsupportedOperationException() override val schedulerService: SchedulerService diff --git a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt index 174dab96f7..25b93c07e3 100644 --- a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt @@ -60,7 +60,8 @@ class ArtemisMessagingTests { var messagingClient: NodeMessagingClient? = null var messagingServer: ArtemisMessagingServer? = null - val networkMapCache = InMemoryNetworkMapCache() + // TODO: We should have a dummy service hub rather than change behaviour in tests + val networkMapCache = InMemoryNetworkMapCache(serviceHub = null) val rpcOps = object : RPCOps { override val protocolVersion: Int get() = throw UnsupportedOperationException() diff --git a/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt index 8eed029284..55f7f6d6aa 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt @@ -6,6 +6,7 @@ import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.services.IdentityService import net.corda.core.utilities.* +import net.corda.flows.AnonymisedIdentity import net.corda.flows.TxKeyFlow import net.corda.node.services.identity.InMemoryIdentityService import net.corda.testing.ALICE_PUBKEY @@ -136,14 +137,14 @@ class InMemoryIdentityServiceTests { } } - private fun createParty(x500Name: X500Name, ca: CertificateAndKeyPair): Pair { + private fun createParty(x500Name: X500Name, ca: CertificateAndKeyPair): Pair { val certFactory = CertificateFactory.getInstance("X509") val issuerKeyPair = generateKeyPair() val issuer = getTestPartyAndCertificate(x500Name, issuerKeyPair.public, ca) val txKey = Crypto.generateKeyPair() val txCert = X509Utilities.createCertificate(CertificateType.IDENTITY, issuer.certificate, issuerKeyPair, x500Name, txKey.public) val txCertPath = certFactory.generateCertPath(listOf(txCert.cert) + issuer.certPath.certificates) - return Pair(issuer, TxKeyFlow.AnonymousIdentity(txCertPath, txCert, AnonymousParty(txKey.public))) + return Pair(issuer, AnonymisedIdentity(txCertPath, txCert, AnonymousParty(txKey.public))) } /** diff --git a/node/src/test/kotlin/net/corda/node/services/network/InMemoryNetworkMapCacheTest.kt b/node/src/test/kotlin/net/corda/node/services/network/InMemoryNetworkMapCacheTest.kt index 3a5f677305..79239ca0ed 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/InMemoryNetworkMapCacheTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/InMemoryNetworkMapCacheTest.kt @@ -1,11 +1,13 @@ package net.corda.node.services.network import net.corda.core.getOrThrow +import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.ServiceInfo import net.corda.core.utilities.ALICE import net.corda.core.utilities.BOB import net.corda.node.utilities.transaction import net.corda.testing.node.MockNetwork +import org.junit.After import org.junit.Test import java.math.BigInteger import kotlin.test.assertEquals @@ -13,6 +15,11 @@ import kotlin.test.assertEquals class InMemoryNetworkMapCacheTest { private val mockNet = MockNetwork() + @After + fun teardown() { + mockNet.stopNodes() + } + @Test fun registerWithNetwork() { val (n0, n1) = mockNet.createTwoNodes() @@ -28,6 +35,8 @@ class InMemoryNetworkMapCacheTest { val nodeB = mockNet.createNode(null, -1, MockNetwork.DefaultFactory, true, BOB.name, null, entropy, ServiceInfo(NetworkMapService.type)) assertEquals(nodeA.info.legalIdentity, nodeB.info.legalIdentity) + mockNet.runNetwork() + // Node A currently knows only about itself, so this returns node A assertEquals(nodeA.netMapCache.getNodeByLegalIdentityKey(nodeA.info.legalIdentity.owningKey), nodeA.info) @@ -37,4 +46,17 @@ class InMemoryNetworkMapCacheTest { // The details of node B write over those for node A assertEquals(nodeA.netMapCache.getNodeByLegalIdentityKey(nodeA.info.legalIdentity.owningKey), nodeB.info) } + + @Test + fun `getNodeByLegalIdentity`() { + val (n0, n1) = mockNet.createTwoNodes() + val node0Cache: NetworkMapCache = n0.services.networkMapCache + val expected = n1.info + + mockNet.runNetwork() + val actual = node0Cache.getNodeByLegalIdentity(n1.info.legalIdentity) + assertEquals(expected, actual) + + // TODO: Should have a test case with anonymous lookup + } } diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt index 2497fcd06f..409ae70201 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt @@ -328,10 +328,11 @@ class FlowFrameworkTests { 2000.DOLLARS, OpaqueBytes.of(0x01), node1.info.legalIdentity, - notary1.info.notaryIdentity)) + notary1.info.notaryIdentity, + anonymous = false)) // We pay a couple of times, the notary picking should go round robin for (i in 1..3) { - node1.services.startFlow(CashPaymentFlow(500.DOLLARS, node2.info.legalIdentity)) + node1.services.startFlow(CashPaymentFlow(500.DOLLARS, node2.info.legalIdentity, anonymous = false)) mockNet.runNetwork() } val endpoint = mockNet.messagingNetwork.endpoint(notary1.network.myAddress as InMemoryMessagingNetwork.PeerHandle)!! diff --git a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaHttpAPITest.kt b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaHttpAPITest.kt index 2887b84435..6305f03104 100644 --- a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaHttpAPITest.kt +++ b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaHttpAPITest.kt @@ -19,8 +19,9 @@ class BankOfCordaHttpAPITest { startNode(BOC.name, setOf(ServiceInfo(SimpleNotaryService.type))), startNode(BIGCORP_LEGAL_NAME) ).getOrThrow() + val anonymous = true val nodeBankOfCordaApiAddr = startWebserver(nodeBankOfCorda).getOrThrow().listenAddress - assertTrue(BankOfCordaClientApi(nodeBankOfCordaApiAddr).requestWebIssue(IssueRequestParams(1000, "USD", BIGCORP_LEGAL_NAME, "1", BOC.name))) + assertTrue(BankOfCordaClientApi(nodeBankOfCordaApiAddr).requestWebIssue(IssueRequestParams(1000, "USD", BIGCORP_LEGAL_NAME, "1", BOC.name, anonymous))) }, isDebug = true) } } diff --git a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt index 7609ce2bb9..ba3ce39b22 100644 --- a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt +++ b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt @@ -39,25 +39,28 @@ class BankOfCordaRPCClientTest { val vaultUpdatesBigCorp = bigCorpProxy.vaultAndUpdates().second // Kick-off actual Issuer Flow + // TODO: Update checks below to reflect states consumed/produced under anonymisation + val anonymous = false bocProxy.startFlow( ::IssuanceRequester, 1000.DOLLARS, nodeBigCorporation.nodeInfo.legalIdentity, BIG_CORP_PARTY_REF, - nodeBankOfCorda.nodeInfo.legalIdentity).returnValue.getOrThrow() + nodeBankOfCorda.nodeInfo.legalIdentity, + anonymous).returnValue.getOrThrow() // Check Bank of Corda Vault Updates vaultUpdatesBoc.expectEvents { sequence( // ISSUE expect { update -> - require(update.consumed.isEmpty()) { update.consumed.size } - require(update.produced.size == 1) { update.produced.size } + require(update.consumed.isEmpty()) { "Expected 0 consumed states, actual: $update" } + require(update.produced.size == 1) { "Expected 1 produced states, actual: $update" } }, // MOVE expect { update -> - require(update.consumed.size == 1) { update.consumed.size } - require(update.produced.isEmpty()) { update.produced.size } + require(update.consumed.size == 1) { "Expected 1 consumed states, actual: $update" } + require(update.produced.isEmpty()) { "Expected 0 produced states, actual: $update" } } ) } diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt index 8c54ac4804..d905bc06b5 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt @@ -68,7 +68,8 @@ private class BankOfCordaDriver { }, isDebug = true) } else { try { - val requestParams = IssueRequestParams(options.valueOf(quantity), options.valueOf(currency), BIGCORP_LEGAL_NAME, "1", BOC.name) + val anonymous = true + val requestParams = IssueRequestParams(options.valueOf(quantity), options.valueOf(currency), BIGCORP_LEGAL_NAME, "1", BOC.name, anonymous) when (role) { Role.ISSUE_CASH_RPC -> { println("Requesting Cash via RPC ...") diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt index b4fef75d86..0aa1647925 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt @@ -44,7 +44,7 @@ class BankOfCordaClientApi(val hostAndPort: HostAndPort) { val amount = Amount(params.amount, currency(params.currency)) val issuerToPartyRef = OpaqueBytes.of(params.issueToPartyRefAsString.toByte()) - return proxy.startFlow(::IssuanceRequester, amount, issueToParty, issuerToPartyRef, issuerBankParty).returnValue.getOrThrow() + return proxy.startFlow(::IssuanceRequester, amount, issueToParty, issuerToPartyRef, issuerBankParty, params.anonymous).returnValue.getOrThrow().stx } } } diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt index 6aba4f1be5..4e2e515a52 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt @@ -20,7 +20,8 @@ import javax.ws.rs.core.Response class BankOfCordaWebApi(val rpc: CordaRPCOps) { data class IssueRequestParams(val amount: Long, val currency: String, val issueToPartyName: X500Name, val issueToPartyRefAsString: String, - val issuerBankName: X500Name) + val issuerBankName: X500Name, + val anonymous: Boolean) private companion object { val logger = loggerFor() @@ -48,11 +49,12 @@ class BankOfCordaWebApi(val rpc: CordaRPCOps) { val amount = Amount(params.amount, currency(params.currency)) val issuerToPartyRef = OpaqueBytes.of(params.issueToPartyRefAsString.toByte()) + val anonymous = params.anonymous // invoke client side of Issuer Flow: IssuanceRequester // The line below blocks and waits for the future to resolve. val status = try { - rpc.startFlow(::IssuanceRequester, amount, issueToParty, issuerToPartyRef, issuerBankParty).returnValue.getOrThrow() + rpc.startFlow(::IssuanceRequester, amount, issueToParty, issuerToPartyRef, issuerBankParty, anonymous).returnValue.getOrThrow() logger.info("Issue request completed successfully: $params") Response.Status.CREATED } catch (e: FlowException) { diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/StateRevisionFlow.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/StateRevisionFlow.kt index 889a7071b1..5cda09ea03 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/StateRevisionFlow.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/StateRevisionFlow.kt @@ -8,6 +8,7 @@ import net.corda.core.transactions.SignedTransaction import net.corda.flows.AbstractStateReplacementFlow import net.corda.flows.StateReplacementException import net.corda.vega.contracts.RevisionedState +import java.security.PublicKey /** * Flow that generates an update on a mutable deal state and commits the resulting transaction reaching consensus @@ -16,13 +17,16 @@ import net.corda.vega.contracts.RevisionedState object StateRevisionFlow { class Requester(curStateRef: StateAndRef>, updatedData: T) : AbstractStateReplacementFlow.Instigator, RevisionedState, T>(curStateRef, updatedData) { - override fun assembleTx(): Pair> { + override fun assembleTx(): AbstractStateReplacementFlow.UpgradeTx { val state = originalState.state.data val tx = state.generateRevision(originalState.state.notary, originalState, modification) tx.addTimeWindow(serviceHub.clock.instant(), 30.seconds) val stx = serviceHub.signInitialTransaction(tx) - return Pair(stx, state.participants) + val participantKeys = state.participants.map { it.owningKey } + // TODO: We need a much faster way of finding our key in the transaction + val myKey = serviceHub.keyManagementService.filterMyKeys(participantKeys).single() + return AbstractStateReplacementFlow.UpgradeTx(stx, participantKeys, myKey) } } diff --git a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt index cc575dd8b1..4c1507159a 100644 --- a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt +++ b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt @@ -51,7 +51,8 @@ class TraderDemoTest : NodeBasedTest() { val expectedBCash = clientB.cashCount + 1 val expectedPaper = listOf(clientA.commercialPaperCount + 1, clientB.commercialPaperCount) - clientA.runBuyer(amount = 100.DOLLARS) + // TODO: Enable anonymisation + clientA.runBuyer(amount = 100.DOLLARS, anonymous = false) clientB.runSeller(counterparty = nodeA.info.legalIdentity.name, amount = 5.DOLLARS) assertThat(clientA.cashCount).isGreaterThan(originalACash) diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt index aca2ca11da..caba7d5252 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt @@ -43,14 +43,14 @@ class TraderDemoClientApi(val rpc: CordaRPCOps) { return vault.filterStatesOfType().size } - fun runBuyer(amount: Amount = 30000.DOLLARS) { + fun runBuyer(amount: Amount = 30000.DOLLARS, anonymous: Boolean = true) { val bankOfCordaParty = rpc.partyFromX500Name(BOC.name) ?: throw Exception("Unable to locate ${BOC.name} in Network Map Service") val me = rpc.nodeIdentity() val amounts = calculateRandomlySizedAmounts(amount, 3, 10, Random()) // issuer random amounts of currency totaling 30000.DOLLARS in parallel val resultFutures = amounts.map { pennies -> - rpc.startFlow(::IssuanceRequester, Amount(pennies, amount.token), me.legalIdentity, OpaqueBytes.of(1), bankOfCordaParty).returnValue + rpc.startFlow(::IssuanceRequester, Amount(pennies, amount.token), me.legalIdentity, OpaqueBytes.of(1), bankOfCordaParty, anonymous).returnValue } Futures.allAsList(resultFutures).getOrThrow() diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt index 84e3c3e745..ce33be8a44 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt @@ -5,6 +5,7 @@ import com.google.common.net.HostAndPort import net.corda.core.crypto.entropyToKeyPair import net.corda.core.identity.Party import net.corda.core.node.NodeInfo +import net.corda.core.node.ServiceHub import net.corda.core.node.services.NetworkMapCache import net.corda.core.utilities.getTestPartyAndCertificate import net.corda.node.services.network.InMemoryNetworkMapCache @@ -17,7 +18,7 @@ import java.math.BigInteger /** * Network map cache with no backing map service. */ -class MockNetworkMapCache : InMemoryNetworkMapCache() { +class MockNetworkMapCache(serviceHub: ServiceHub) : InMemoryNetworkMapCache(serviceHub) { private companion object { val BANK_C = getTestPartyAndCertificate(getTestX509Name("Bank C"), entropyToKeyPair(BigInteger.valueOf(1000)).public) val BANK_D = getTestPartyAndCertificate(getTestX509Name("Bank D"), entropyToKeyPair(BigInteger.valueOf(2000)).public) diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt index c3dd7e6e1f..1fdd752414 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -15,6 +15,7 @@ import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.DUMMY_CA import net.corda.core.utilities.getTestPartyAndCertificate +import net.corda.flows.AnonymisedIdentity import net.corda.node.services.database.HibernateConfiguration import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.keys.freshCertificate @@ -104,7 +105,9 @@ class MockKeyManagementService(val identityService: IdentityService, return k.public } - override fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): Pair { + override fun filterMyKeys(candidateKeys: Iterable): Iterable = candidateKeys.filter { it in this.keys } + + override fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): AnonymisedIdentity { return freshCertificate(identityService, freshKey(), identity, getSigner(identity.owningKey), revocationEnabled) } diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt b/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt index 70f66961ca..6731e35c5c 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt @@ -43,7 +43,8 @@ class SimpleNode(val config: NodeConfiguration, val address: HostAndPort = freeL val identityService: IdentityService = InMemoryIdentityService(trustRoot = trustRoot) val keyService: KeyManagementService = E2ETestKeyManagementService(identityService, setOf(identity)) val executor = ServiceAffinityExecutor(config.myLegalName.commonName, 1) - val broker = ArtemisMessagingServer(config, address.port, rpcAddress.port, InMemoryNetworkMapCache(), userService) + // TODO: We should have a dummy service hub rather than change behaviour in tests + val broker = ArtemisMessagingServer(config, address.port, rpcAddress.port, InMemoryNetworkMapCache(serviceHub = null), userService) val networkMapRegistrationFuture: SettableFuture = SettableFuture.create() val network = database.transaction { NodeMessagingClient( diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt index 9a7ada66ea..7ea256d5cd 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt @@ -22,11 +22,7 @@ import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.ALICE import net.corda.core.utilities.BOB import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.flows.CashExitFlow -import net.corda.flows.CashFlowCommand -import net.corda.flows.CashIssueFlow -import net.corda.flows.CashPaymentFlow -import net.corda.flows.IssuerFlow +import net.corda.flows.* import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.PortAllocation import net.corda.testing.driver.driver @@ -136,10 +132,11 @@ class ExplorerSimulation(val options: OptionSet) { private fun startSimulation(eventGenerator: EventGenerator, maxIterations: Int) { // Log to logger when flow finish. - fun FlowHandle.log(seq: Int, name: String) { + fun FlowHandle.log(seq: Int, name: String) { val out = "[$seq] $name $id :" returnValue.success { - Main.log.info("$out ${it.id} ${(it.tx.outputs.first().data as Cash.State).amount}") + val (stx, idenities) = it + Main.log.info("$out ${stx.id} ${(stx.tx.outputs.first().data as Cash.State).amount}") }.failure { Main.log.info("$out ${it.message}") } @@ -179,11 +176,12 @@ class ExplorerSimulation(val options: OptionSet) { currencies = listOf(GBP, USD) ) val maxIterations = 100_000 + val anonymous = true // Pre allocate some money to each party. eventGenerator.parties.forEach { for (ref in 0..1) { for ((currency, issuer) in issuers) { - CashFlowCommand.IssueCash(Amount(1_000_000, currency), OpaqueBytes(ByteArray(1, { ref.toByte() })), it, notaryNode.nodeInfo.notaryIdentity).startFlow(issuer) + CashFlowCommand.IssueCash(Amount(1_000_000, currency), OpaqueBytes(ByteArray(1, { ref.toByte() })), it, notaryNode.nodeInfo.notaryIdentity, anonymous).startFlow(issuer) } } } diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt index 36ed583c7c..eed9b556e6 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt @@ -20,14 +20,16 @@ import net.corda.client.jfx.utils.unique import net.corda.core.contracts.Amount import net.corda.core.contracts.sumOrNull import net.corda.core.contracts.withoutIssuer -import net.corda.core.identity.AbstractParty -import net.corda.core.identity.Party import net.corda.core.flows.FlowException import net.corda.core.getOrThrow +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.Party +import net.corda.core.messaging.FlowHandle import net.corda.core.messaging.startFlow import net.corda.core.node.NodeInfo import net.corda.core.serialization.OpaqueBytes import net.corda.core.then +import net.corda.core.transactions.SignedTransaction import net.corda.explorer.formatters.PartyNameFormatter import net.corda.explorer.model.CashTransaction import net.corda.explorer.model.IssuerModel @@ -35,6 +37,7 @@ import net.corda.explorer.model.ReportingCurrencyModel import net.corda.explorer.views.bigDecimalFormatter import net.corda.explorer.views.byteFormatter import net.corda.explorer.views.stringConverter +import net.corda.flows.AbstractCashFlow import net.corda.flows.CashFlowCommand import net.corda.flows.IssuerFlow.IssuanceRequester import org.controlsfx.dialog.ExceptionDialog @@ -92,18 +95,20 @@ class NewTransaction : Fragment() { initOwner(window) show() } - val handle = if (command is CashFlowCommand.IssueCash) { + val handle: FlowHandle = if (command is CashFlowCommand.IssueCash) { rpcProxy.value!!.startFlow(::IssuanceRequester, command.amount, command.recipient, command.issueRef, - myIdentity.value!!.legalIdentity) + myIdentity.value!!.legalIdentity, + command.anonymous) } else { command.startFlow(rpcProxy.value!!) } runAsync { handle.returnValue.then { dialog.dialogPane.isDisable = false }.getOrThrow() - }.ui { + }.ui { it -> + val stx: SignedTransaction = it.stx val type = when (command) { is CashFlowCommand.IssueCash -> "Cash Issued" is CashFlowCommand.ExitCash -> "Cash Exited" @@ -117,7 +122,7 @@ class NewTransaction : Fragment() { row { label(type) { font = Font.font(font.family, FontWeight.EXTRA_BOLD, font.size + 2) } } row { label("Transaction ID :") { GridPane.setValignment(this, VPos.TOP) } - label { text = Splitter.fixedLength(16).split("${it.id}").joinToString("\n") } + label { text = Splitter.fixedLength(16).split("${stx.id}").joinToString("\n") } } } dialog.dialogPane.scene.window.sizeToScene() @@ -141,14 +146,16 @@ class NewTransaction : Fragment() { dialogPane = root initOwner(window) setResultConverter { + // TODO: Enable confidential identities + val anonymous = false val defaultRef = OpaqueBytes.of(1) val issueRef = if (issueRef.value != null) OpaqueBytes.of(issueRef.value) else defaultRef when (it) { executeButton -> when (transactionTypeCB.value) { CashTransaction.Issue -> { - CashFlowCommand.IssueCash(Amount.fromDecimal(amount.value, currencyChoiceBox.value), issueRef, partyBChoiceBox.value.legalIdentity, notaries.first().notaryIdentity) + CashFlowCommand.IssueCash(Amount.fromDecimal(amount.value, currencyChoiceBox.value), issueRef, partyBChoiceBox.value.legalIdentity, notaries.first().notaryIdentity, anonymous) } - CashTransaction.Pay -> CashFlowCommand.PayCash(Amount.fromDecimal(amount.value, currencyChoiceBox.value), partyBChoiceBox.value.legalIdentity) + CashTransaction.Pay -> CashFlowCommand.PayCash(Amount.fromDecimal(amount.value, currencyChoiceBox.value), partyBChoiceBox.value.legalIdentity, anonymous = anonymous) CashTransaction.Exit -> CashFlowCommand.ExitCash(Amount.fromDecimal(amount.value, currencyChoiceBox.value), issueRef) else -> null } diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/CrossCashTest.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/CrossCashTest.kt index dbe922e5a6..b0268fefe2 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/CrossCashTest.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/CrossCashTest.kt @@ -117,13 +117,14 @@ val crossCashTest = LoadTest( generate = { (nodeVaults), parallelism -> val nodeMap = simpleNodes.associateBy { it.info.legalIdentity } + val anonymous = true Generator.pickN(parallelism, simpleNodes).bind { nodes -> Generator.sequence( nodes.map { node -> val quantities = nodeVaults[node.info.legalIdentity] ?: mapOf() val possibleRecipients = nodeMap.keys.toList() val moves = quantities.map { - it.value.toDouble() / 1000 to generateMove(it.value, USD, node.info.legalIdentity, possibleRecipients) + it.value.toDouble() / 1000 to generateMove(it.value, USD, node.info.legalIdentity, possibleRecipients, anonymous) } val exits = quantities.mapNotNull { if (it.key == node.info.legalIdentity) { @@ -133,7 +134,7 @@ val crossCashTest = LoadTest( } } val command = Generator.frequency( - listOf(1.0 to generateIssue(10000, USD, notary.info.notaryIdentity, possibleRecipients)) + moves + exits + listOf(1.0 to generateIssue(10000, USD, notary.info.notaryIdentity, possibleRecipients, anonymous)) + moves + exits ) command.map { CrossCashCommand(it, nodeMap[node.info.legalIdentity]!!) } } diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/GenerateHelpers.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/GenerateHelpers.kt index 9794ddb9b9..32442df9c1 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/GenerateHelpers.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/GenerateHelpers.kt @@ -15,13 +15,14 @@ fun generateIssue( max: Long, currency: Currency, notary: Party, - possibleRecipients: List + possibleRecipients: List, + anonymous: Boolean ): Generator { return generateAmount(1, max, Generator.pure(currency)).combine( Generator.pure(OpaqueBytes.of(0)), Generator.pickOne(possibleRecipients) ) { amount, ref, recipient -> - CashFlowCommand.IssueCash(amount, ref, recipient, notary) + CashFlowCommand.IssueCash(amount, ref, recipient, notary, anonymous) } } @@ -29,12 +30,13 @@ fun generateMove( max: Long, currency: Currency, issuer: Party, - possibleRecipients: List + possibleRecipients: List, + anonymous: Boolean ): Generator { return generateAmount(1, max, Generator.pure(Issued(PartyAndReference(issuer, OpaqueBytes.of(0)), currency))).combine( Generator.pickOne(possibleRecipients) ) { amount, recipient -> - CashFlowCommand.PayCash(amount.withoutIssuer(), recipient, issuer) + CashFlowCommand.PayCash(amount.withoutIssuer(), recipient, issuer, anonymous) } } diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/SelfIssueTest.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/SelfIssueTest.kt index 6c298b981a..0543cd83b4 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/SelfIssueTest.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/SelfIssueTest.kt @@ -38,7 +38,7 @@ val selfIssueTest = LoadTest( generate = { _, parallelism -> val generateIssue = Generator.pickOne(simpleNodes).bind { node -> - generateIssue(1000, USD, notary.info.notaryIdentity, listOf(node.info.legalIdentity)).map { + generateIssue(1000, USD, notary.info.notaryIdentity, listOf(node.info.legalIdentity), anonymous = true).map { SelfIssueCommand(it, node) } } diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/StabilityTest.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/StabilityTest.kt index 500283f630..08c8f48766 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/StabilityTest.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/StabilityTest.kt @@ -19,7 +19,7 @@ object StabilityTest { val nodeMap = simpleNodes.associateBy { it.info.legalIdentity } Generator.sequence(simpleNodes.map { node -> val possibleRecipients = nodeMap.keys.toList() - val moves = 0.5 to generateMove(1, USD, node.info.legalIdentity, possibleRecipients) + val moves = 0.5 to generateMove(1, USD, node.info.legalIdentity, possibleRecipients, anonymous = true) val exits = 0.5 to generateExit(1, USD) val command = Generator.frequency(listOf(moves, exits)) command.map { CrossCashCommand(it, nodeMap[node.info.legalIdentity]!!) } @@ -42,7 +42,7 @@ object StabilityTest { "Self issuing cash randomly", generate = { _, parallelism -> val generateIssue = Generator.pickOne(simpleNodes).bind { node -> - generateIssue(1000, USD, notary.info.notaryIdentity, listOf(node.info.legalIdentity)).map { + generateIssue(1000, USD, notary.info.notaryIdentity, listOf(node.info.legalIdentity), anonymous = true).map { SelfIssueCommand(it, node) } } From 75e82094a2be01b41f6537094e7cdef32500fa6c Mon Sep 17 00:00:00 2001 From: David Lee Date: Wed, 28 Jun 2017 15:37:17 +0100 Subject: [PATCH 16/97] Updated trademark notice Updated reference to R3 legal entity --- TRADEMARK | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TRADEMARK b/TRADEMARK index be0b5c8260..21b2d63a2d 100644 --- a/TRADEMARK +++ b/TRADEMARK @@ -1,4 +1,4 @@ -Corda and the Corda logo are trademarks of R3CEV LLC and its affiliates. +Corda and the Corda logo are trademarks of R3 HoldCo LLC and its affiliates. All rights reserved. -For R3CEV LLC's trademark and logo usage information, please consult our Trademark Usage Policy available at https://www.r3.com/trademark-usage-policy \ No newline at end of file +For R3 HoldCo LLC's trademark and logo usage information, please consult our Trademark Usage Policy available at https://www.r3.com/trademark-usage-policy From bd08d6c6f86f78aa730bf69c03cd556e41d84668 Mon Sep 17 00:00:00 2001 From: Konstantinos Chalkias Date: Wed, 28 Jun 2017 15:55:43 +0100 Subject: [PATCH 17/97] change to 0.14-SNAPSHOT (#928) --- build.gradle | 2 +- docs/source/changelog.rst | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 6bc81509ac..62f0783ca8 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { file("$projectDir/constants.properties").withInputStream { constants.load(it) } // Our version: bump this on release. - ext.corda_release_version = "0.13-SNAPSHOT" + ext.corda_release_version = "0.14-SNAPSHOT" // Increment this on any release that changes public APIs anywhere in the Corda platform // TODO This is going to be difficult until we have a clear separation throughout the code of what is public and what is internal ext.corda_platform_version = 1 diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index ae8ee4e2e9..ee30fee6c3 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -84,7 +84,6 @@ support for more currencies to the DemoBench and Explorer tools. to specify for individual nodes. * Dependencies changes: - * Upgraded Kotlin to v1.1.2. * Upgraded Dokka to v0.9.14. * Upgraded Gradle Plugins to 0.12.4. * Upgraded Apache ActiveMQ Artemis to v2.1.0. From 556444f9e1e2e3079d01861f416e327e215e8d32 Mon Sep 17 00:00:00 2001 From: josecoll Date: Wed, 28 Jun 2017 16:00:39 +0100 Subject: [PATCH 18/97] Cleaned up incorrect references to vault query and persistence. (#933) * Cleaned up incorrect references to vault query and persistence. * Consistent use of :doc: reference. --- docs/source/api-index.rst | 2 +- docs/source/{api-vault.rst => api-vault-query.rst} | 9 ++++----- docs/source/node-internals-index.rst | 1 - docs/source/release-notes.rst | 2 +- docs/source/vault.rst | 2 +- 5 files changed, 7 insertions(+), 9 deletions(-) rename docs/source/{api-vault.rst => api-vault-query.rst} (96%) diff --git a/docs/source/api-index.rst b/docs/source/api-index.rst index bdec7b5a9e..8498f42e9b 100644 --- a/docs/source/api-index.rst +++ b/docs/source/api-index.rst @@ -6,7 +6,7 @@ This section describes the APIs that are available for the development of CorDap * :doc:`api-states` * :doc:`api-persistence` * :doc:`api-contracts` -* :doc:`api-vault` +* :doc:`api-vault-query` * :doc:`api-transactions` * :doc:`api-flows` * :doc:`api-core-types` diff --git a/docs/source/api-vault.rst b/docs/source/api-vault-query.rst similarity index 96% rename from docs/source/api-vault.rst rename to docs/source/api-vault-query.rst index 44706e1c15..3651f06e44 100644 --- a/docs/source/api-vault.rst +++ b/docs/source/api-vault-query.rst @@ -1,5 +1,5 @@ -API: Vault -========== +API: Vault Query +================ Corda has been architected from the ground up to encourage usage of industry standard, proven query frameworks and libraries for accessing RDBMS backed transactional stores (including the Vault). @@ -66,7 +66,7 @@ There are four implementations of this interface which can be chained together t .. note:: All contract states that extend ``LinearState`` or ``DealState`` now automatically persist those interfaces common state attributes to the **vault_linear_states** table. - 4. ``VaultCustomQueryCriteria`` provides the means to specify one or many arbitrary expressions on attributes defined by a custom contract state that implements its own schema as described in the api-persistence_ documentation and associated examples. Custom criteria expressions are expressed using one of several type-safe ``CriteriaExpression``: BinaryLogical, Not, ColumnPredicateExpression. The ColumnPredicateExpression allows for specification arbitrary criteria using the previously enumerated operator types. Furthermore, a rich DSL is provided to enable simple construction of custom criteria using any combination of ``ColumnPredicate``. + 4. ``VaultCustomQueryCriteria`` provides the means to specify one or many arbitrary expressions on attributes defined by a custom contract state that implements its own schema as described in the :doc:`Persistence ` documentation and associated examples. Custom criteria expressions are expressed using one of several type-safe ``CriteriaExpression``: BinaryLogical, Not, ColumnPredicateExpression. The ColumnPredicateExpression allows for specification arbitrary criteria using the previously enumerated operator types. Furthermore, a rich DSL is provided to enable simple construction of custom criteria using any combination of ``ColumnPredicate``. .. note:: It is a requirement to register any custom contract schemas to be used in Vault Custom queries in the associated `CordaPluginRegistry` configuration for the respective CorDapp using the ``requiredSchemas`` configuration field (which specifies a set of `MappedSchema`) @@ -85,10 +85,9 @@ Examples of these ``QueryCriteria`` objects are presented below for Kotlin and J .. note:: When specifying the Contract Type as a parameterised type to the QueryCriteria in Kotlin, queries now include all concrete implementations of that type if this is an interface. Previously, it was only possible to query on Concrete types (or the universe of all Contract States). -The Vault Query API leverages the rich semantics of the underlying JPA Hibernate_ based Persistence_ framework adopted by Corda. +The Vault Query API leverages the rich semantics of the underlying JPA Hibernate_ based :doc:`Persistence ` framework adopted by Corda. .. _Hibernate: https://docs.jboss.org/hibernate/jpa/2.1/api/ -.. _Persistence: https://docs.corda.net/api-persistence.html .. note:: Permissioning at the database level will be enforced at a later date to ensure authenticated, role-based, read-only access to underlying Corda tables. diff --git a/docs/source/node-internals-index.rst b/docs/source/node-internals-index.rst index 8fe4795988..e5a1c9157a 100644 --- a/docs/source/node-internals-index.rst +++ b/docs/source/node-internals-index.rst @@ -8,4 +8,3 @@ Node internals vault serialization messaging - persistence \ No newline at end of file diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index 6ea371d138..bd89729765 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -22,7 +22,7 @@ state schemas defined by CorDapp developers when modelling new contract types. C simple but sophisticated builder DSL (see ``QueryCriteriaUtils``). The new Vault Query service is usable by flows and by RPC clients alike via two simple API functions: ``queryBy()`` and ``trackBy()``. The former provides point-in-time snapshot queries whilst the later supplements the snapshot with dynamic streaming of updates. -See :doc:`vault-query` for full details. +See :doc:`api-vault-query` for full details. We have written a comprehensive Hello, World! tutorial, showing developers how to build a CorDapp from start to finish. The tutorial shows how the core elements of a CorDapp - states, contracts and flows - fit together diff --git a/docs/source/vault.rst b/docs/source/vault.rst index c5a55638b6..37bf233843 100644 --- a/docs/source/vault.rst +++ b/docs/source/vault.rst @@ -46,7 +46,7 @@ Note the following: * the vault performs fungible state spending (and in future, fungible state optimisation management including merging, splitting and re-issuance) * vault extensions represent additional custom plugin code a developer may write to query specific custom contract state attributes. * customer "Off Ledger" (private store) represents internal organisational data that may be joined with the vault data to perform additional reporting or processing -* a :doc:`vault query API ` is exposed to developers using standard Corda RPC and CorDapp plugin mechanisms +* a :doc:`Vault Query API ` is exposed to developers using standard Corda RPC and CorDapp plugin mechanisms * a vault update API is internally used by transaction recording flows. * the vault database schemas are directly accessible via JDBC for customer joins and queries From 89e20d6746a7d849001af22b168e4ccf3a506227 Mon Sep 17 00:00:00 2001 From: Clinton Alexander Date: Wed, 28 Jun 2017 17:57:16 +0100 Subject: [PATCH 19/97] Removing repository addition - this can really mess with the ability to control a project's repositories. --- .../src/main/groovy/net/corda/plugins/QuasarPlugin.groovy | 5 ----- 1 file changed, 5 deletions(-) diff --git a/gradle-plugins/quasar-utils/src/main/groovy/net/corda/plugins/QuasarPlugin.groovy b/gradle-plugins/quasar-utils/src/main/groovy/net/corda/plugins/QuasarPlugin.groovy index 0ed4fe5935..5083554337 100644 --- a/gradle-plugins/quasar-utils/src/main/groovy/net/corda/plugins/QuasarPlugin.groovy +++ b/gradle-plugins/quasar-utils/src/main/groovy/net/corda/plugins/QuasarPlugin.groovy @@ -10,11 +10,6 @@ import org.gradle.api.tasks.JavaExec */ class QuasarPlugin implements Plugin { void apply(Project project) { - - project.repositories { - mavenCentral() - } - project.configurations.create("quasar") // To add a local .jar dependency: // project.dependencies.add("quasar", project.files("${project.rootProject.projectDir}/lib/quasar.jar")) From 380841553f59fcbb258657179b08ed7c8233ee6a Mon Sep 17 00:00:00 2001 From: Clinton Alexander Date: Wed, 28 Jun 2017 17:57:51 +0100 Subject: [PATCH 20/97] Bumped corda gradle plugins to 0.13.0 --- constants.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constants.properties b/constants.properties index 1676023e4a..e1c47af61a 100644 --- a/constants.properties +++ b/constants.properties @@ -1,4 +1,4 @@ -gradlePluginsVersion=0.12.4 +gradlePluginsVersion=0.13.0 kotlinVersion=1.1.1 guavaVersion=21.0 bouncycastleVersion=1.57 From 79823531cf082095d34d60d975127747015e9cda Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Wed, 28 Jun 2017 17:21:50 +0100 Subject: [PATCH 21/97] Minor: update log message --- .../net/corda/node/services/messaging/NodeMessagingClient.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt index bb46e06be6..d3773f68b7 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt @@ -158,7 +158,7 @@ class NodeMessagingClient(override val config: NodeConfiguration, check(!started) { "start can't be called twice" } started = true - log.info("Connecting to server: $serverAddress") + log.info("Connecting to message broker: $serverAddress") // TODO Add broker CN to config for host verification in case the embedded broker isn't used val tcpTransport = ArtemisTcpTransport.tcpTransport(ConnectionDirection.Outbound(), serverAddress, config) val locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport) From 698fe7846a245cfdb9cbe910a63a2ceee15989b5 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Wed, 28 Jun 2017 18:01:55 +0100 Subject: [PATCH 22/97] Correct usage of notary legal and service identities StandaloneCordaRPClientTest attempted to use a notary legal identity as service identity, which fails as the service is separate. This separates the two usages correctly. --- .../kotlin/rpc/StandaloneCordaRPClientTest.kt | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt index 3c0c055655..e2cdf44958 100644 --- a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt +++ b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt @@ -10,8 +10,8 @@ import net.corda.core.contracts.POUNDS import net.corda.core.contracts.SWISS_FRANCS import net.corda.core.crypto.SecureHash import net.corda.core.getOrThrow -import net.corda.core.identity.Party import net.corda.core.messaging.* +import net.corda.core.node.NodeInfo import net.corda.core.node.services.Vault import net.corda.core.node.services.vault.PageSpecification import net.corda.core.node.services.vault.QueryCriteria @@ -51,7 +51,7 @@ class StandaloneCordaRPClientTest { private lateinit var notary: NodeProcess private lateinit var rpcProxy: CordaRPCOps private lateinit var connection: CordaRPCConnection - private lateinit var notaryIdentity: Party + private lateinit var notaryNode: NodeInfo private val notaryConfig = NodeConfig( party = DUMMY_NOTARY, @@ -67,7 +67,7 @@ class StandaloneCordaRPClientTest { notary = NodeProcess.Factory().create(notaryConfig) connection = notary.connect() rpcProxy = connection.proxy - notaryIdentity = fetchNotaryIdentity() + notaryNode = fetchNotaryIdentity() } @After @@ -95,7 +95,7 @@ class StandaloneCordaRPClientTest { @Test fun `test starting flow`() { - rpcProxy.startFlow(::CashIssueFlow, 127.POUNDS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity) + rpcProxy.startFlow(::CashIssueFlow, 127.POUNDS, OpaqueBytes.of(0), notaryNode.legalIdentity, notaryNode.notaryIdentity) .returnValue.getOrThrow(timeout) } @@ -103,7 +103,7 @@ class StandaloneCordaRPClientTest { fun `test starting tracked flow`() { var trackCount = 0 val handle = rpcProxy.startTrackedFlow( - ::CashIssueFlow, 429.DOLLARS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity + ::CashIssueFlow, 429.DOLLARS, OpaqueBytes.of(0), notaryNode.legalIdentity, notaryNode.notaryIdentity ) handle.progress.subscribe { msg -> log.info("Flow>> $msg") @@ -115,7 +115,7 @@ class StandaloneCordaRPClientTest { @Test fun `test network map`() { - assertEquals(DUMMY_NOTARY.name, notaryIdentity.name) + assertEquals(DUMMY_NOTARY.name, notaryNode.legalIdentity.name) } @Test @@ -132,7 +132,7 @@ class StandaloneCordaRPClientTest { } // Now issue some cash - rpcProxy.startFlow(::CashIssueFlow, 513.SWISS_FRANCS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity) + rpcProxy.startFlow(::CashIssueFlow, 513.SWISS_FRANCS, OpaqueBytes.of(0), notaryNode.legalIdentity, notaryNode.notaryIdentity) .returnValue.getOrThrow(timeout) assertEquals(1, updateCount) } @@ -149,7 +149,7 @@ class StandaloneCordaRPClientTest { } // Now issue some cash - rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity) + rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryNode.legalIdentity, notaryNode.notaryIdentity) .returnValue.getOrThrow(timeout) assertNotEquals(0, updateCount) @@ -172,7 +172,7 @@ class StandaloneCordaRPClientTest { } // Now issue some cash - rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity) + rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryNode.legalIdentity, notaryNode.notaryIdentity) .returnValue.getOrThrow(timeout) assertNotEquals(0, updateCount) @@ -186,7 +186,7 @@ class StandaloneCordaRPClientTest { @Test fun `test vault query by`() { // Now issue some cash - rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity) + rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryNode.legalIdentity, notaryNode.notaryIdentity) .returnValue.getOrThrow(timeout) val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL) @@ -197,7 +197,7 @@ class StandaloneCordaRPClientTest { assertEquals(1, queryResults.totalStatesAvailable) assertEquals(queryResults.states.first().state.data.amount.quantity, 629.POUNDS.quantity) - rpcProxy.startFlow(::CashPaymentFlow, 100.POUNDS, notaryIdentity).returnValue.getOrThrow() + rpcProxy.startFlow(::CashPaymentFlow, 100.POUNDS, notaryNode.legalIdentity).returnValue.getOrThrow() val moreResults = rpcProxy.vaultQueryBy(criteria, paging, sorting) assertEquals(3, moreResults.totalStatesAvailable) // 629 - 100 + 100 @@ -209,11 +209,11 @@ class StandaloneCordaRPClientTest { assertEquals(629.POUNDS, cashBalance[Currency.getInstance("GBP")]) } - private fun fetchNotaryIdentity(): Party { - val (nodeInfo, nodeUpdates) = rpcProxy.networkMapUpdates() + private fun fetchNotaryIdentity(): NodeInfo { + val (nodeInfo, nodeUpdates) = rpcProxy.networkMapFeed() nodeUpdates.notUsed() assertEquals(1, nodeInfo.size) - return nodeInfo[0].legalIdentity + return nodeInfo[0] } // This InputStream cannot have been whitelisted. From b791530b2836cd639c6cefbfda2e474b919366b1 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Wed, 28 Jun 2017 17:43:54 +0100 Subject: [PATCH 23/97] Correct trackBy() call Correct trackBy() call on vault service, which should call _trackBy() but previously called _queryBy() --- core/src/main/kotlin/net/corda/core/node/services/Services.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/node/services/Services.kt b/core/src/main/kotlin/net/corda/core/node/services/Services.kt index 39d8d53a4f..07198e8ab2 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/Services.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/Services.kt @@ -401,8 +401,8 @@ interface VaultQueryService { return _queryBy(criteria, paging, sorting, contractType) } - fun trackBy(contractType: Class): Vault.Page { - return _queryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractType) + fun trackBy(contractType: Class): DataFeed, Vault.Update> { + return _trackBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractType) } fun trackBy(contractType: Class, criteria: QueryCriteria): DataFeed, Vault.Update> { return _trackBy(criteria, PageSpecification(), Sort(emptySet()), contractType) From 5a45459b9d25abc5033e72de67e061009874b023 Mon Sep 17 00:00:00 2001 From: Rick Parker Date: Thu, 29 Jun 2017 14:03:41 +0100 Subject: [PATCH 24/97] AMQP serialization part 3: some custom serializers, integration with Kryo (disabled) (#859) --- .../core/serialization/CordaClassResolver.kt | 39 ++- .../core/serialization/KryoAMQPSerializer.kt | 51 ++++ .../amqp/AMQPPrimitiveSerializer.kt | 14 +- .../serialization/amqp/ArraySerializer.kt | 16 +- .../amqp/CollectionSerializer.kt | 2 +- .../serialization/amqp/CustomSerializer.kt | 84 +++++- .../amqp/DeserializationInput.kt | 56 ++-- .../amqp/DeserializedParameterizedType.kt | 5 +- .../core/serialization/amqp/MapSerializer.kt | 2 +- .../serialization/amqp/ObjectSerializer.kt | 9 +- .../serialization/amqp/PropertySerializer.kt | 50 ++-- .../corda/core/serialization/amqp/Schema.kt | 104 ++++--- .../serialization/amqp/SerializationHelper.kt | 96 +++++-- .../serialization/amqp/SerializerFactory.kt | 255 ++++++++++++------ .../serialization/amqp/SingletonSerializer.kt | 32 +++ .../amqp/custom/BigDecimalSerializer.kt | 11 + .../amqp/custom/CurrencySerializer.kt | 12 + .../amqp/custom/InstantSerializer.kt | 18 ++ .../amqp/custom/PublicKeySerializer.kt | 14 +- .../amqp/custom/X500NameSerializer.kt | 25 ++ .../amqp/SerializationOutputTests.kt | 197 +++++++++++++- 21 files changed, 854 insertions(+), 238 deletions(-) create mode 100644 core/src/main/kotlin/net/corda/core/serialization/KryoAMQPSerializer.kt create mode 100644 core/src/main/kotlin/net/corda/core/serialization/amqp/SingletonSerializer.kt create mode 100644 core/src/main/kotlin/net/corda/core/serialization/amqp/custom/BigDecimalSerializer.kt create mode 100644 core/src/main/kotlin/net/corda/core/serialization/amqp/custom/CurrencySerializer.kt create mode 100644 core/src/main/kotlin/net/corda/core/serialization/amqp/custom/InstantSerializer.kt create mode 100644 core/src/main/kotlin/net/corda/core/serialization/amqp/custom/X500NameSerializer.kt diff --git a/core/src/main/kotlin/net/corda/core/serialization/CordaClassResolver.kt b/core/src/main/kotlin/net/corda/core/serialization/CordaClassResolver.kt index d9755cec29..c97087025a 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/CordaClassResolver.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/CordaClassResolver.kt @@ -29,7 +29,11 @@ fun makeAllButBlacklistedClassResolver(): ClassResolver { return CordaClassResolver(AllButBlacklisted) } -class CordaClassResolver(val whitelist: ClassWhitelist) : DefaultClassResolver() { +/** + * @param amqpEnabled Setting this to true turns on experimental AMQP serialization for any class annotated with + * [CordaSerializable]. + */ +class CordaClassResolver(val whitelist: ClassWhitelist, val amqpEnabled: Boolean = false) : DefaultClassResolver() { /** Returns the registration for the specified class, or null if the class is not registered. */ override fun getRegistration(type: Class<*>): Registration? { return super.getRegistration(type) ?: checkClass(type) @@ -59,7 +63,7 @@ class CordaClassResolver(val whitelist: ClassWhitelist) : DefaultClassResolver() return checkClass(type.superclass) } // It's safe to have the Class already, since Kryo loads it with initialisation off. - // If we use a whitelist with blacklisting capabilities, whitelist.hasListed(type) may throw a NotSerializableException if input class is blacklisted. + // If we use a whitelist with blacklisting capabilities, whitelist.hasListed(type) may throw an IllegalStateException if input class is blacklisted. // Thus, blacklisting precedes annotation checking. if (!whitelist.hasListed(type) && !checkForAnnotation(type)) { throw KryoException("Class ${Util.className(type)} is not annotated or on the whitelist, so cannot be used in serialization") @@ -68,13 +72,22 @@ class CordaClassResolver(val whitelist: ClassWhitelist) : DefaultClassResolver() } override fun registerImplicit(type: Class<*>): Registration { - // We have to set reference to true, since the flag influences how String fields are treated and we want it to be consistent. - val references = kryo.references - try { - kryo.references = true - return register(Registration(type, kryo.getDefaultSerializer(type), NAME.toInt())) - } finally { - kryo.references = references + val hasAnnotation = checkForAnnotation(type) + // If something is not annotated, or AMQP is disabled, we stay serializing with Kryo. This will typically be the + // case for flow checkpoints (ignoring all cases where AMQP is disabled) since our top level messaging data structures + // are annotated and once we enter AMQP serialisation we stay with it for the entire object subgraph. + if (!hasAnnotation || !amqpEnabled) { + // We have to set reference to true, since the flag influences how String fields are treated and we want it to be consistent. + val references = kryo.references + try { + kryo.references = true + return register(Registration(type, kryo.getDefaultSerializer(type), NAME.toInt())) + } finally { + kryo.references = references + } + } else { + // Build AMQP serializer + return register(Registration(type, KryoAMQPSerializer, NAME.toInt())) } } @@ -85,13 +98,13 @@ class CordaClassResolver(val whitelist: ClassWhitelist) : DefaultClassResolver() return (type.classLoader !is AttachmentsClassLoader) && !KryoSerializable::class.java.isAssignableFrom(type) && !type.isAnnotationPresent(DefaultSerializer::class.java) - && (type.isAnnotationPresent(CordaSerializable::class.java) || hasAnnotationOnInterface(type)) + && (type.isAnnotationPresent(CordaSerializable::class.java) || hasInheritedAnnotation(type)) } // Recursively check interfaces for our annotation. - private fun hasAnnotationOnInterface(type: Class<*>): Boolean { - return type.interfaces.any { it.isAnnotationPresent(CordaSerializable::class.java) || hasAnnotationOnInterface(it) } - || (type.superclass != null && hasAnnotationOnInterface(type.superclass)) + private fun hasInheritedAnnotation(type: Class<*>): Boolean { + return type.interfaces.any { it.isAnnotationPresent(CordaSerializable::class.java) || hasInheritedAnnotation(it) } + || (type.superclass != null && hasInheritedAnnotation(type.superclass)) } // Need to clear out class names from attachments. diff --git a/core/src/main/kotlin/net/corda/core/serialization/KryoAMQPSerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/KryoAMQPSerializer.kt new file mode 100644 index 0000000000..42cb65aec7 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/serialization/KryoAMQPSerializer.kt @@ -0,0 +1,51 @@ +package net.corda.core.serialization + +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.Serializer +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import net.corda.core.serialization.amqp.DeserializationInput +import net.corda.core.serialization.amqp.SerializationOutput +import net.corda.core.serialization.amqp.SerializerFactory + +/** + * This [Kryo] custom [Serializer] switches the object graph of anything annotated with `@CordaSerializable` + * to using the AMQP serialization wire format, and simply writes that out as bytes to the wire. + * + * There is no need to write out the length, since this can be peeked out of the first few bytes of the stream. + */ +object KryoAMQPSerializer : Serializer() { + internal fun registerCustomSerializers(factory: SerializerFactory) { + factory.apply { + register(net.corda.core.serialization.amqp.custom.PublicKeySerializer) + register(net.corda.core.serialization.amqp.custom.ThrowableSerializer(this)) + register(net.corda.core.serialization.amqp.custom.X500NameSerializer) + register(net.corda.core.serialization.amqp.custom.BigDecimalSerializer) + register(net.corda.core.serialization.amqp.custom.CurrencySerializer) + register(net.corda.core.serialization.amqp.custom.InstantSerializer(this)) + } + } + + // TODO: need to sort out the whitelist... we currently do not apply the whitelist attached to the [Kryo] + // instance to the factory. We need to do this before turning on AMQP serialization. + private val serializerFactory = SerializerFactory().apply { + registerCustomSerializers(this) + } + + override fun write(kryo: Kryo, output: Output, obj: Any) { + val amqpOutput = SerializationOutput(serializerFactory) + val bytes = amqpOutput.serialize(obj).bytes + // No need to write out the size since it's encoded within the AMQP. + output.write(bytes) + } + + override fun read(kryo: Kryo, input: Input, type: Class): Any { + val amqpInput = DeserializationInput(serializerFactory) + // Use our helper functions to peek the size of the serialized object out of the AMQP byte stream. + val peekedBytes = input.readBytes(DeserializationInput.BYTES_NEEDED_TO_PEEK) + val size = DeserializationInput.peekSize(peekedBytes) + val allBytes = peekedBytes.copyOf(size) + input.readBytes(allBytes, peekedBytes.size, size - peekedBytes.size) + return amqpInput.deserialize(SerializedBytes(allBytes), type) + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/AMQPPrimitiveSerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/AMQPPrimitiveSerializer.kt index 40f586a88e..b68d37c935 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/amqp/AMQPPrimitiveSerializer.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/AMQPPrimitiveSerializer.kt @@ -1,14 +1,16 @@ package net.corda.core.serialization.amqp -import com.google.common.primitives.Primitives +import org.apache.qpid.proton.amqp.Binary import org.apache.qpid.proton.codec.Data import java.lang.reflect.Type /** * Serializer / deserializer for native AMQP types (Int, Float, String etc). + * + * [ByteArray] is automatically marshalled to/from the Proton-J wrapper, [Binary]. */ class AMQPPrimitiveSerializer(clazz: Class<*>) : AMQPSerializer { - override val typeDescriptor: String = SerializerFactory.primitiveTypeName(Primitives.wrap(clazz))!! + override val typeDescriptor: String = SerializerFactory.primitiveTypeName(clazz)!! override val type: Type = clazz // NOOP since this is a primitive type. @@ -16,8 +18,12 @@ class AMQPPrimitiveSerializer(clazz: Class<*>) : AMQPSerializer { } override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) { - data.putObject(obj) + if (obj is ByteArray) { + data.putObject(Binary(obj)) + } else { + data.putObject(obj) + } } - override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = obj + override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = (obj as? Binary)?.array ?: obj } \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/ArraySerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/ArraySerializer.kt index 0cf705e16d..ca1612bc50 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/amqp/ArraySerializer.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/ArraySerializer.kt @@ -2,8 +2,6 @@ package net.corda.core.serialization.amqp import org.apache.qpid.proton.codec.Data import java.io.NotSerializableException -import java.lang.reflect.GenericArrayType -import java.lang.reflect.ParameterizedType import java.lang.reflect.Type /** @@ -12,14 +10,10 @@ import java.lang.reflect.Type class ArraySerializer(override val type: Type, factory: SerializerFactory) : AMQPSerializer { override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}" - internal val elementType: Type = makeElementType() + internal val elementType: Type = type.componentType() private val typeNotation: TypeNotation = RestrictedType(type.typeName, null, emptyList(), "list", Descriptor(typeDescriptor, null), emptyList()) - private fun makeElementType(): Type { - return (type as? Class<*>)?.componentType ?: (type as GenericArrayType).genericComponentType - } - override fun writeClassInfo(output: SerializationOutput) { if (output.writeTypeNotations(typeNotation)) { output.requireSerializer(elementType) @@ -44,13 +38,7 @@ class ArraySerializer(override val type: Type, factory: SerializerFactory) : AMQ } private fun List.toArrayOfType(type: Type): Any { - val elementType: Class<*> = if (type is Class<*>) { - type - } else if (type is ParameterizedType) { - type.rawType as Class<*> - } else { - throw NotSerializableException("Unexpected array element type $type") - } + val elementType = type.asClass() ?: throw NotSerializableException("Unexpected array element type $type") val list = this return java.lang.reflect.Array.newInstance(elementType, this.size).apply { val array = this diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/CollectionSerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/CollectionSerializer.kt index 0f4421de6c..76ec0be975 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/amqp/CollectionSerializer.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/CollectionSerializer.kt @@ -32,7 +32,7 @@ class CollectionSerializer(val declaredType: ParameterizedType, factory: Seriali private val concreteBuilder: (List<*>) -> Collection<*> = findConcreteType(declaredType.rawType as Class<*>) - private val typeNotation: TypeNotation = RestrictedType(declaredType.toString(), null, emptyList(), "list", Descriptor(typeDescriptor, null), emptyList()) + private val typeNotation: TypeNotation = RestrictedType(SerializerFactory.nameForType(declaredType), null, emptyList(), "list", Descriptor(typeDescriptor, null), emptyList()) override fun writeClassInfo(output: SerializationOutput) { if (output.writeTypeNotations(typeNotation)) { diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/CustomSerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/CustomSerializer.kt index e88230de3d..d08d3b8e88 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/amqp/CustomSerializer.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/CustomSerializer.kt @@ -1,5 +1,6 @@ package net.corda.core.serialization.amqp +import net.corda.core.serialization.amqp.SerializerFactory.Companion.nameForType import org.apache.qpid.proton.codec.Data import java.lang.reflect.Type @@ -10,11 +11,16 @@ import java.lang.reflect.Type abstract class CustomSerializer : AMQPSerializer { /** * This is a collection of custom serializers that this custom serializer depends on. e.g. for proxy objects - * that refer to arrays of types etc. + * that refer to other custom types etc. */ abstract val additionalSerializers: Iterable> + /** + * This method should return true if the custom serializer can serialize an instance of the class passed as the + * parameter. + */ abstract fun isSerializerFor(clazz: Class<*>): Boolean + protected abstract val descriptor: Descriptor /** * This exists purely for documentation and cross-platform purposes. It is not used by our serialization / deserialization @@ -32,12 +38,42 @@ abstract class CustomSerializer : AMQPSerializer { abstract fun writeDescribedObject(obj: T, data: Data, type: Type, output: SerializationOutput) /** - * Additional base features for a custom serializer that is a particular class. + * This custom serializer represents a sort of symbolic link from a subclass to a super class, where the super + * class custom serializer is responsible for the "on the wire" format but we want to create a reference to the + * subclass in the schema, so that we can distinguish between subclasses. + */ + // TODO: should this be a custom serializer at all, or should it just be a plain AMQPSerializer? + class SubClass(protected val clazz: Class<*>, protected val superClassSerializer: CustomSerializer) : CustomSerializer() { + override val additionalSerializers: Iterable> = emptyList() + // TODO: should this be empty or contain the schema of the super? + override val schemaForDocumentation = Schema(emptyList()) + + override fun isSerializerFor(clazz: Class<*>): Boolean = clazz == this.clazz + override val type: Type get() = clazz + override val typeDescriptor: String = "$DESCRIPTOR_DOMAIN:${fingerprintForDescriptors(superClassSerializer.typeDescriptor, nameForType(clazz))}" + private val typeNotation: TypeNotation = RestrictedType(SerializerFactory.nameForType(clazz), null, emptyList(), SerializerFactory.nameForType(superClassSerializer.type), Descriptor(typeDescriptor, null), emptyList()) + override fun writeClassInfo(output: SerializationOutput) { + output.writeTypeNotations(typeNotation) + } + + override val descriptor: Descriptor = Descriptor(typeDescriptor) + + override fun writeDescribedObject(obj: T, data: Data, type: Type, output: SerializationOutput) { + superClassSerializer.writeDescribedObject(obj, data, type, output) + } + + override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): T { + return superClassSerializer.readObject(obj, schema, input) + } + } + + /** + * Additional base features for a custom serializer for a particular class, that excludes subclasses. */ abstract class Is(protected val clazz: Class) : CustomSerializer() { override fun isSerializerFor(clazz: Class<*>): Boolean = clazz == this.clazz override val type: Type get() = clazz - override val typeDescriptor: String = "$DESCRIPTOR_DOMAIN:${clazz.name}" + override val typeDescriptor: String = "$DESCRIPTOR_DOMAIN:${nameForType(clazz)}" override fun writeClassInfo(output: SerializationOutput) {} override val descriptor: Descriptor = Descriptor(typeDescriptor) } @@ -48,13 +84,13 @@ abstract class CustomSerializer : AMQPSerializer { abstract class Implements(protected val clazz: Class) : CustomSerializer() { override fun isSerializerFor(clazz: Class<*>): Boolean = this.clazz.isAssignableFrom(clazz) override val type: Type get() = clazz - override val typeDescriptor: String = "$DESCRIPTOR_DOMAIN:${clazz.name}" + override val typeDescriptor: String = "$DESCRIPTOR_DOMAIN:${nameForType(clazz)}" override fun writeClassInfo(output: SerializationOutput) {} override val descriptor: Descriptor = Descriptor(typeDescriptor) } /** - * Addition base features over and above [Implements] or [Is] custom serializer for when the serialize form should be + * Additional base features over and above [Implements] or [Is] custom serializer for when the serialized form should be * the serialized form of a proxy class, and the object can be re-created from that proxy on deserialization. * * The proxy class must use only types which are either native AMQP or other types for which there are pre-registered @@ -66,14 +102,14 @@ abstract class CustomSerializer : AMQPSerializer { 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 val typeDescriptor: String = "$DESCRIPTOR_DOMAIN:${nameForType(clazz)}" 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)) + val typeNotations = mutableSetOf(CompositeType(nameForType(type), null, emptyList(), descriptor, (proxySerializer.typeNotation as CompositeType).fields)) for (additional in additionalSerializers) { typeNotations.addAll(additional.schemaForDocumentation.types) } @@ -102,4 +138,38 @@ abstract class CustomSerializer : AMQPSerializer { return fromProxy(proxy) } } + + /** + * A custom serializer where the on-wire representation is a string. For example, a [Currency] might be represented + * as a 3 character currency code, and converted to and from that string. By default, it is assumed that the + * [toString] method will generate the string representation and that there is a constructor that takes such a + * string as an argument to reconstruct. + * + * @param clazz The type to be marshalled + * @param withInheritance Whether subclasses of the class can also be marshalled. + * @param make A lambda for constructing an instance, that defaults to calling a constructor that expects a string. + * @param unmake A lambda that extracts the string value for an instance, that defaults to the [toString] method. + */ + abstract class ToString(clazz: Class, withInheritance: Boolean = false, + private val maker: (String) -> T = clazz.getConstructor(String::class.java).let { `constructor` -> { string -> `constructor`.newInstance(string) } }, + private val unmaker: (T) -> String = { obj -> obj.toString() }) : Proxy(clazz, String::class.java, /* Unused */ SerializerFactory(), withInheritance) { + + override val additionalSerializers: Iterable> = emptyList() + + override val schemaForDocumentation = Schema(listOf(RestrictedType(nameForType(type), "", listOf(nameForType(type)), SerializerFactory.primitiveTypeName(String::class.java)!!, descriptor, emptyList()))) + + override fun toProxy(obj: T): String = unmaker(obj) + + override fun fromProxy(proxy: String): T = maker(proxy) + + override fun writeDescribedObject(obj: T, data: Data, type: Type, output: SerializationOutput) { + val proxy = toProxy(obj) + data.putObject(proxy) + } + + override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): T { + val proxy = input.readObject(obj, schema, String::class.java) as String + 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 ccbe1fac20..3258375fad 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 @@ -2,7 +2,9 @@ package net.corda.core.serialization.amqp import com.google.common.base.Throwables import net.corda.core.serialization.SerializedBytes +import org.apache.qpid.proton.amqp.Binary import org.apache.qpid.proton.amqp.DescribedType +import org.apache.qpid.proton.amqp.UnsignedByte import org.apache.qpid.proton.codec.Data import java.io.NotSerializableException import java.lang.reflect.Type @@ -19,6 +21,41 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory = S // TODO: we're not supporting object refs yet private val objectHistory: MutableList = ArrayList() + internal companion object { + val BYTES_NEEDED_TO_PEEK: Int = 23 + + private fun subArraysEqual(a: ByteArray, aOffset: Int, length: Int, b: ByteArray, bOffset: Int): Boolean { + if (aOffset + length > a.size || bOffset + length > b.size) throw IndexOutOfBoundsException() + var bytesRemaining = length + var aPos = aOffset + var bPos = bOffset + while (bytesRemaining-- > 0) { + if (a[aPos++] != b[bPos++]) return false + } + return true + } + + fun peekSize(bytes: ByteArray): Int { + // There's an 8 byte header, and then a 0 byte plus descriptor followed by constructor + val eighth = bytes[8].toInt() + check(eighth == 0x0) { "Expected to find a descriptor in the AMQP stream" } + // We should always have an Envelope, so the descriptor should be a 64-bit long (0x80) + val ninth = UnsignedByte.valueOf(bytes[9]).toInt() + check(ninth == 0x80) { "Expected to find a ulong in the AMQP stream" } + // Skip 8 bytes + val eighteenth = UnsignedByte.valueOf(bytes[18]).toInt() + check(eighteenth == 0xd0 || eighteenth == 0xc0) { "Expected to find a list8 or list32 in the AMQP stream" } + val size = if (eighteenth == 0xc0) { + // Next byte is size + UnsignedByte.valueOf(bytes[19]).toInt() - 3 // Minus three as PEEK_SIZE assumes 4 byte unsigned integer. + } else { + // Next 4 bytes is size + UnsignedByte.valueOf(bytes[19]).toInt().shl(24) + UnsignedByte.valueOf(bytes[20]).toInt().shl(16) + UnsignedByte.valueOf(bytes[21]).toInt().shl(8) + UnsignedByte.valueOf(bytes[22]).toInt() + } + return size + BYTES_NEEDED_TO_PEEK + } + } + @Throws(NotSerializableException::class) inline fun deserialize(bytes: SerializedBytes): T = deserialize(bytes, T::class.java) @@ -66,25 +103,10 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory = S 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 if (obj is Binary) { + return obj.array } else { return obj } } - - private fun Type.isSubClassOf(type: Type): Boolean { - return type == Object::class.java || - (this is Class<*> && type is Class<*> && type.isAssignableFrom(this)) || - (this is DeserializedParameterizedType && type is Class<*> && this.rawType == type && this.isFullyWildcarded) - } - - private fun subArraysEqual(a: ByteArray, aOffset: Int, length: Int, b: ByteArray, bOffset: Int): Boolean { - if (aOffset + length > a.size || bOffset + length > b.size) throw IndexOutOfBoundsException() - var bytesRemaining = length - var aPos = aOffset - var bPos = bOffset - while (bytesRemaining-- > 0) { - if (a[aPos++] != b[bPos++]) return false - } - return true - } } \ No newline at end of file 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 9a0809d18d..8869d9c758 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 @@ -1,5 +1,6 @@ package net.corda.core.serialization.amqp +import com.google.common.primitives.Primitives import java.io.NotSerializableException import java.lang.reflect.ParameterizedType import java.lang.reflect.Type @@ -119,7 +120,9 @@ 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 { + Primitives.wrap(SerializerFactory.primitiveType(typeName) ?: 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 7991648f1a..95803f3070 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 @@ -31,7 +31,7 @@ class MapSerializer(val declaredType: ParameterizedType, factory: SerializerFact private val concreteBuilder: (Map<*, *>) -> Map<*, *> = findConcreteType(declaredType.rawType as Class<*>) - private val typeNotation: TypeNotation = RestrictedType(declaredType.toString(), null, emptyList(), "map", Descriptor(typeDescriptor, null), emptyList()) + private val typeNotation: TypeNotation = RestrictedType(SerializerFactory.nameForType(declaredType), null, emptyList(), "map", Descriptor(typeDescriptor, null), emptyList()) override fun writeClassInfo(output: SerializationOutput) { if (output.writeTypeNotations(typeNotation)) { 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 130d50d7a3..d22c968ef6 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 @@ -1,5 +1,6 @@ package net.corda.core.serialization.amqp +import net.corda.core.serialization.amqp.SerializerFactory.Companion.nameForType import org.apache.qpid.proton.amqp.UnsignedInteger import org.apache.qpid.proton.codec.Data import java.io.NotSerializableException @@ -10,7 +11,7 @@ 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<*>, factory: SerializerFactory) : AMQPSerializer { +class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPSerializer { override val type: Type get() = clazz private val javaConstructor: Constructor? internal val propertySerializers: Collection @@ -20,7 +21,9 @@ class ObjectSerializer(val clazz: Class<*>, factory: SerializerFactory) : AMQPSe javaConstructor = kotlinConstructor?.javaConstructor propertySerializers = propertiesForSerialization(kotlinConstructor, clazz, factory) } - private val typeName = clazz.name + + private val typeName = nameForType(clazz) + 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. @@ -65,7 +68,7 @@ class ObjectSerializer(val clazz: Class<*>, factory: SerializerFactory) : AMQPSe } private fun generateProvides(): List { - return interfaces.map { it.typeName } + return interfaces.map { nameForType(it) } } 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 2295a07b45..4020ca5cc5 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 @@ -1,14 +1,16 @@ package net.corda.core.serialization.amqp +import org.apache.qpid.proton.amqp.Binary import org.apache.qpid.proton.codec.Data import java.lang.reflect.Method +import java.lang.reflect.Type import kotlin.reflect.full.memberProperties import kotlin.reflect.jvm.javaGetter /** * Base class for serialization of a property of an object. */ -sealed class PropertySerializer(val name: String, val readMethod: Method) { +sealed class PropertySerializer(val name: String, val readMethod: Method, val resolvedType: Type) { abstract fun writeClassInfo(output: SerializationOutput) abstract fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) abstract fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? @@ -18,23 +20,20 @@ sealed class PropertySerializer(val name: String, val readMethod: Method) { val default: String? = generateDefault() val mandatory: Boolean = generateMandatory() - private val isInterface: Boolean get() = (readMethod.genericReturnType as? Class<*>)?.isInterface ?: false - private val isJVMPrimitive: Boolean get() = (readMethod.genericReturnType as? Class<*>)?.isPrimitive ?: false + private val isInterface: Boolean get() = resolvedType.asClass()?.isInterface ?: false + private val isJVMPrimitive: Boolean get() = resolvedType.asClass()?.isPrimitive ?: false private fun generateType(): String { - return if (isInterface) "*" else { - val primitiveName = SerializerFactory.primitiveTypeName(readMethod.genericReturnType) - return primitiveName ?: readMethod.genericReturnType.typeName - } + return if (isInterface || resolvedType == Any::class.java) "*" else SerializerFactory.nameForType(resolvedType) } private fun generateRequires(): List { - return if (isInterface) listOf(readMethod.genericReturnType.typeName) else emptyList() + return if (isInterface) listOf(SerializerFactory.nameForType(resolvedType)) else emptyList() } private fun generateDefault(): String? { if (isJVMPrimitive) { - return when (readMethod.genericReturnType) { + return when (resolvedType) { java.lang.Boolean.TYPE -> "false" java.lang.Character.TYPE -> "�" else -> "0" @@ -54,13 +53,12 @@ sealed class PropertySerializer(val name: String, val readMethod: Method) { } companion object { - fun make(name: String, readMethod: Method, factory: SerializerFactory): PropertySerializer { - val type = readMethod.genericReturnType - if (SerializerFactory.isPrimitive(type)) { + fun make(name: String, readMethod: Method, resolvedType: Type, factory: SerializerFactory): PropertySerializer { + if (SerializerFactory.isPrimitive(resolvedType)) { // 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) + return AMQPPrimitivePropertySerializer(name, readMethod, resolvedType) } else { - return DescribedTypePropertySerializer(name, readMethod) { factory.get(null, type) } + return DescribedTypePropertySerializer(name, readMethod, resolvedType) { factory.get(null, resolvedType) } } } } @@ -68,35 +66,43 @@ sealed class PropertySerializer(val name: String, val readMethod: Method) { /** * A property serializer for a complex type (another object). */ - class DescribedTypePropertySerializer(name: String, readMethod: Method, private val lazyTypeSerializer: () -> AMQPSerializer) : PropertySerializer(name, readMethod) { + class DescribedTypePropertySerializer(name: String, readMethod: Method, resolvedType: Type, private val lazyTypeSerializer: () -> AMQPSerializer<*>) : PropertySerializer(name, readMethod, resolvedType) { // 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() } + private val typeSerializer: AMQPSerializer<*> by lazy { lazyTypeSerializer() } override fun writeClassInfo(output: SerializationOutput) { - typeSerializer.writeClassInfo(output) + if (resolvedType != Any::class.java) { + typeSerializer.writeClassInfo(output) + } } override fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? { - return input.readObjectOrNull(obj, schema, readMethod.genericReturnType) + return input.readObjectOrNull(obj, schema, resolvedType) } override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) { - output.writeObjectOrNull(readMethod.invoke(obj), data, readMethod.genericReturnType) + output.writeObjectOrNull(readMethod.invoke(obj), data, resolvedType) } } /** * A property serializer for an AMQP primitive type (Int, String, etc). */ - class AMQPPrimitivePropertySerializer(name: String, readMethod: Method) : PropertySerializer(name, readMethod) { + class AMQPPrimitivePropertySerializer(name: String, readMethod: Method, resolvedType: Type) : PropertySerializer(name, readMethod, resolvedType) { + override fun writeClassInfo(output: SerializationOutput) {} override fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? { - return obj + return if (obj is Binary) obj.array else obj } override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) { - data.putObject(readMethod.invoke(obj)) + val value = readMethod.invoke(obj) + if (value is ByteArray) { + data.putObject(Binary(value)) + } else { + data.putObject(value) + } } } } 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 5c627cc943..507c2f0a6f 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 @@ -2,7 +2,7 @@ package net.corda.core.serialization.amqp import com.google.common.hash.Hasher import com.google.common.hash.Hashing -import net.corda.core.crypto.Base58 +import net.corda.core.crypto.toBase64 import net.corda.core.serialization.OpaqueBytes import org.apache.qpid.proton.amqp.DescribedType import org.apache.qpid.proton.amqp.UnsignedLong @@ -12,6 +12,8 @@ import java.io.NotSerializableException import java.lang.reflect.GenericArrayType import java.lang.reflect.ParameterizedType import java.lang.reflect.Type +import java.lang.reflect.TypeVariable +import java.util.* // TODO: get an assigned number as per AMQP spec val DESCRIPTOR_TOP_32BITS: Long = 0xc0da0000 @@ -310,6 +312,7 @@ private val ALREADY_SEEN_HASH: String = "Already seen = true" private val NULLABLE_HASH: String = "Nullable = true" private val NOT_NULLABLE_HASH: String = "Nullable = false" private val ANY_TYPE_HASH: String = "Any type = true" +private val TYPE_VARIABLE_HASH: String = "Type variable = true" /** * The method generates a fingerprint for a given JVM [Type] that should be unique to the schema representation. @@ -320,44 +323,83 @@ private val ANY_TYPE_HASH: String = "Any type = true" * different. */ // TODO: write tests -internal fun fingerprintForType(type: Type, factory: SerializerFactory): String = Base58.encode(fingerprintForType(type, HashSet(), Hashing.murmur3_128().newHasher(), factory).hash().asBytes()) +internal fun fingerprintForType(type: Type, factory: SerializerFactory): String { + return fingerprintForType(type, null, HashSet(), Hashing.murmur3_128().newHasher(), factory).hash().asBytes().toBase64() +} -private fun fingerprintForType(type: Type, alreadySeen: MutableSet, hasher: Hasher, factory: SerializerFactory): Hasher { +internal fun fingerprintForDescriptors(vararg typeDescriptors: String): String { + val hasher = Hashing.murmur3_128().newHasher() + for (typeDescriptor in typeDescriptors) { + hasher.putUnencodedChars(typeDescriptor) + } + return hasher.hash().asBytes().toBase64() +} + +// This method concatentates various elements of the types recursively as unencoded strings into the hasher, effectively +// creating a unique string for a type which we then hash in the calling function above. +private fun fingerprintForType(type: Type, contextType: Type?, alreadySeen: MutableSet, hasher: Hasher, factory: SerializerFactory): Hasher { return if (type in alreadySeen) { hasher.putUnencodedChars(ALREADY_SEEN_HASH) } else { alreadySeen += type - if (type is SerializerFactory.AnyType) { - hasher.putUnencodedChars(ANY_TYPE_HASH) - } else if (type is Class<*>) { - if (type.isArray) { - 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 { - // 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 + try { + if (type is SerializerFactory.AnyType) { + hasher.putUnencodedChars(ANY_TYPE_HASH) + } else if (type is Class<*>) { + if (type.isArray) { + fingerprintForType(type.componentType, contextType, alreadySeen, hasher, factory).putUnencodedChars(ARRAY_HASH) + } else if (SerializerFactory.isPrimitive(type)) { + hasher.putUnencodedChars(type.name) + } else if (isCollectionOrMap(type)) { + hasher.putUnencodedChars(type.name) } else { - hasher.putUnencodedChars(customSerializer.typeDescriptor) + // Need to check if a custom serializer is applicable + val customSerializer = factory.findCustomSerializer(type, type) + if (customSerializer == null) { + if (type.kotlin.objectInstance != null) { + // TODO: name collision is too likely for kotlin objects, we need to introduce some reference + // to the CorDapp but maybe reference to the JAR in the short term. + hasher.putUnencodedChars(type.name) + } else { + fingerprintForObject(type, contextType, alreadySeen, hasher, factory) + } + } else { + hasher.putUnencodedChars(customSerializer.typeDescriptor) + } } + } else if (type is ParameterizedType) { + // Hash the rawType + params + val clazz = type.rawType as Class<*> + val startingHash = if (isCollectionOrMap(clazz)) { + hasher.putUnencodedChars(clazz.name) + } else { + fingerprintForObject(type, type, alreadySeen, hasher, factory) + } + // ... and concatentate the type data for each parameter type. + type.actualTypeArguments.fold(startingHash) { orig, paramType -> fingerprintForType(paramType, type, alreadySeen, orig, factory) } + } else if (type is GenericArrayType) { + // Hash the element type + some array hash + fingerprintForType(type.genericComponentType, contextType, alreadySeen, hasher, factory).putUnencodedChars(ARRAY_HASH) + } else if (type is TypeVariable<*>) { + // TODO: include bounds + hasher.putUnencodedChars(type.name).putUnencodedChars(TYPE_VARIABLE_HASH) + } else { + throw NotSerializableException("Don't know how to hash") } - } else if (type is ParameterizedType) { - // Hash the rawType + params - 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, factory).putUnencodedChars(ARRAY_HASH) - } else { - throw NotSerializableException("Don't know how to hash $type") + } catch(e: NotSerializableException) { + throw NotSerializableException("${e.message} -> $type") } } } + +private fun isCollectionOrMap(type: Class<*>) = Collection::class.java.isAssignableFrom(type) || Map::class.java.isAssignableFrom(type) + +private fun fingerprintForObject(type: Type, contextType: Type?, alreadySeen: MutableSet, hasher: Hasher, factory: SerializerFactory): Hasher { + // Hash the class + properties + interfaces + val name = type.asClass()?.name ?: throw NotSerializableException("Expected only Class or ParameterizedType but found $type") + propertiesForSerialization(constructorForDeserialization(type), contextType ?: type, factory).fold(hasher.putUnencodedChars(name)) { orig, prop -> + fingerprintForType(prop.resolvedType, type, alreadySeen, orig, factory).putUnencodedChars(prop.name).putUnencodedChars(if (prop.mandatory) NOT_NULLABLE_HASH else NULLABLE_HASH) + } + interfacesForSerialization(type).map { fingerprintForType(it, type, alreadySeen, hasher, factory) } + return hasher +} \ No newline at end of file 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 85082544a4..c77faa5119 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 @@ -4,10 +4,8 @@ import com.google.common.reflect.TypeToken import org.apache.qpid.proton.codec.Data import java.beans.Introspector 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 java.lang.reflect.* +import java.util.* import kotlin.reflect.KClass import kotlin.reflect.KFunction import kotlin.reflect.KParameter @@ -29,9 +27,10 @@ annotation class ConstructorForDeserialization * Otherwise it starts with the primary constructor in kotlin, if there is one, and then will override this with any that is * annotated with [@CordaConstructor]. It will report an error if more than one constructor is annotated. */ -internal fun constructorForDeserialization(clazz: Class): KFunction? { +internal fun constructorForDeserialization(type: Type): KFunction? { + val clazz: Class<*> = type.asClass()!! if (isConcrete(clazz)) { - var preferredCandidate: KFunction? = clazz.kotlin.primaryConstructor + var preferredCandidate: KFunction? = clazz.kotlin.primaryConstructor var annotatedCount = 0 val kotlinConstructors = clazz.kotlin.constructors val hasDefault = kotlinConstructors.any { it.parameters.isEmpty() } @@ -60,13 +59,14 @@ 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<*>, factory: SerializerFactory): Collection { - return if (kotlinConstructor != null) propertiesForSerialization(kotlinConstructor, factory) else propertiesForSerialization(clazz, factory) +internal fun propertiesForSerialization(kotlinConstructor: KFunction?, type: Type, factory: SerializerFactory): Collection { + val clazz = type.asClass()!! + return if (kotlinConstructor != null) propertiesForSerializationFromConstructor(kotlinConstructor, type, factory) else propertiesForSerializationFromAbstract(clazz, type, factory) } private fun isConcrete(clazz: Class<*>): Boolean = !(clazz.isInterface || Modifier.isAbstract(clazz.modifiers)) -private fun propertiesForSerialization(kotlinConstructor: KFunction, factory: SerializerFactory): Collection { +private fun propertiesForSerializationFromConstructor(kotlinConstructor: KFunction, type: Type, 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 = Introspector.getBeanInfo(clazz).propertyDescriptors.filter { it.name != "class" }.groupBy { it.name }.mapValues { it.value[0] } @@ -78,10 +78,11 @@ private fun propertiesForSerialization(kotlinConstructor: KFunction // 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.") + val returnType = resolveTypeVariables(getter.genericReturnType, type) if (constructorParamTakesReturnTypeOfGetter(getter, param)) { - rc += PropertySerializer.make(name, getter, factory) + rc += PropertySerializer.make(name, getter, returnType, factory) } else { - throw NotSerializableException("Property type ${getter.genericReturnType} for $name of $clazz differs from constructor parameter type ${param.type.javaType}") + throw NotSerializableException("Property type $returnType for $name of $clazz differs from constructor parameter type ${param.type.javaType}") } } return rc @@ -89,35 +90,36 @@ private fun propertiesForSerialization(kotlinConstructor: KFunction private fun constructorParamTakesReturnTypeOfGetter(getter: Method, param: KParameter): Boolean = TypeToken.of(param.type.javaType).isSupertypeOf(getter.genericReturnType) -private fun propertiesForSerialization(clazz: Class<*>, factory: SerializerFactory): Collection { +private fun propertiesForSerializationFromAbstract(clazz: Class<*>, type: Type, 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, factory) + val returnType = resolveTypeVariables(getter.genericReturnType, type) + rc += PropertySerializer.make(property.name, getter, returnType, factory) } return rc } -internal fun interfacesForSerialization(clazz: Class<*>): List { +internal fun interfacesForSerialization(type: Type): List { val interfaces = LinkedHashSet() - exploreType(clazz, interfaces) + exploreType(type, interfaces) return interfaces.toList() } private fun exploreType(type: Type?, interfaces: MutableSet) { - val clazz = (type as? Class<*>) ?: (type as? ParameterizedType)?.rawType as? Class<*> + val clazz = type?.asClass() if (clazz != null) { - if (clazz.isInterface) interfaces += clazz + if (clazz.isInterface) interfaces += type!! for (newInterface in clazz.genericInterfaces) { if (newInterface !in interfaces) { - interfaces += newInterface - exploreType(newInterface, interfaces) + exploreType(resolveTypeVariables(newInterface, type), interfaces) } } - exploreType(clazz.genericSuperclass, interfaces) + val superClass = clazz.genericSuperclass ?: return + exploreType(resolveTypeVariables(superClass, type), interfaces) } } @@ -143,4 +145,58 @@ fun Data.withList(block: Data.() -> Unit) { enter() block() exit() // exit list +} + +private fun resolveTypeVariables(actualType: Type, contextType: Type?): Type { + val resolvedType = if (contextType != null) TypeToken.of(contextType).resolveType(actualType).type else actualType + // TODO: surely we check it is concrete at this point with no TypeVariables + return if (resolvedType is TypeVariable<*>) { + val bounds = resolvedType.bounds + return if (bounds.isEmpty()) SerializerFactory.AnyType else if (bounds.size == 1) resolveTypeVariables(bounds[0], contextType) else throw NotSerializableException("Got bounded type $actualType but only support single bound.") + } else { + resolvedType + } +} + +internal fun Type.asClass(): Class<*>? { + return if (this is Class<*>) { + this + } else if (this is ParameterizedType) { + this.rawType.asClass() + } else if (this is GenericArrayType) { + this.genericComponentType.asClass()?.arrayClass() + } else null +} + +internal fun Type.asArray(): Type? { + return if (this is Class<*>) { + this.arrayClass() + } else if (this is ParameterizedType) { + DeserializedGenericArrayType(this) + } else null +} + +internal fun Class<*>.arrayClass(): Class<*> = java.lang.reflect.Array.newInstance(this, 0).javaClass + +internal fun Type.isArray(): Boolean = (this is Class<*> && this.isArray) || (this is GenericArrayType) + +internal fun Type.componentType(): Type { + check(this.isArray()) { "$this is not an array type." } + return (this as? Class<*>)?.componentType ?: (this as GenericArrayType).genericComponentType +} + +internal fun Class<*>.asParameterizedType(): ParameterizedType { + return DeserializedParameterizedType(this, this.typeParameters) +} + +internal fun Type.asParameterizedType(): ParameterizedType { + return when (this) { + is Class<*> -> this.asParameterizedType() + is ParameterizedType -> this + else -> throw NotSerializableException("Don't know how to convert to ParameterizedType") + } +} + +internal fun Type.isSubClassOf(type: Type): Boolean { + return TypeToken.of(this).isSubtypeOf(type) } \ No newline at end of file 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 3883aad9dd..a4f887be8b 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 @@ -1,6 +1,7 @@ package net.corda.core.serialization.amqp import com.google.common.primitives.Primitives +import com.google.common.reflect.TypeResolver import net.corda.core.checkNotUnorderedHashMap import net.corda.core.serialization.AllWhitelist import net.corda.core.serialization.ClassWhitelist @@ -20,9 +21,9 @@ import javax.annotation.concurrent.ThreadSafe * Factory of serializers designed to be shared across threads and invocations. */ // TODO: enums -// TODO: object references +// TODO: object references - need better fingerprinting? // TODO: class references? (e.g. cheat with repeated descriptors using a long encoding, like object ref proposal) -// TODO: Inner classes etc +// TODO: Inner classes etc. Should we allow? Currently not considered. // 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 @@ -32,7 +33,13 @@ import javax.annotation.concurrent.ThreadSafe // 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]. +// TODO: generally map Object to '*' all over the place in the schema and make sure use of '*' amd '?' is consistent and documented in generics. +// TODO: found a document that states textual descriptors are Symbols. Adjust schema class appropriately. +// TODO: document and alert to the fact that classes cannot default superclass/interface properties otherwise they are "erased" due to matching with constructor. +// TODO: type name prefixes for interfaces and abstract classes? Or use label? +// TODO: generic types should define restricted type alias with source of the wildcarded version, I think, if we're to generate classes from schema +// TODO: need to rethink matching of constructor to properties in relation to implementing interfaces and needing those properties etc. +// TODO: need to support super classes as well as interfaces with our current code base... what's involved? If we continue to ban, what is the impact? @ThreadSafe class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) { private val serializersByType = ConcurrentHashMap>() @@ -42,44 +49,99 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) { /** * Look up, and manufacture if necessary, a serializer for the given type. * - * @param actualType Will be null if there isn't an actual object instance available (e.g. for + * @param actualClass Will be null if there isn't an actual object instance available (e.g. for * restricted type processing). */ @Throws(NotSerializableException::class) - fun get(actualType: Class<*>?, declaredType: Type): AMQPSerializer { - if (declaredType is ParameterizedType) { - return serializersByType.computeIfAbsent(declaredType) { - // We allow only Collection and Map. - val rawType = declaredType.rawType - if (rawType is Class<*>) { - checkParameterisedTypesConcrete(declaredType.actualTypeArguments) - if (Collection::class.java.isAssignableFrom(rawType)) { - CollectionSerializer(declaredType, this) - } else if (Map::class.java.isAssignableFrom(rawType)) { - makeMapSerializer(declaredType) - } else { - throw NotSerializableException("Declared types of $declaredType are not supported.") - } - } else { - throw NotSerializableException("Declared types of $declaredType are not supported.") + fun get(actualClass: Class<*>?, declaredType: Type): AMQPSerializer { + val declaredClass = declaredType.asClass() + if (declaredClass != null) { + val actualType: Type = inferTypeVariables(actualClass, declaredClass, declaredType) ?: declaredType + if (Collection::class.java.isAssignableFrom(declaredClass)) { + return serializersByType.computeIfAbsent(declaredType) { + CollectionSerializer(declaredType as? ParameterizedType ?: DeserializedParameterizedType(declaredClass, arrayOf(AnyType), null), this) + } + } else if (Map::class.java.isAssignableFrom(declaredClass)) { + return serializersByType.computeIfAbsent(declaredClass) { + makeMapSerializer(declaredType as? ParameterizedType ?: DeserializedParameterizedType(declaredClass, arrayOf(AnyType, AnyType), null)) } - } - } else if (declaredType is Class<*>) { - // Simple classes allowed - if (Collection::class.java.isAssignableFrom(declaredType)) { - 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) + return makeClassSerializer(actualClass ?: declaredClass, actualType, declaredType) } - } else if (declaredType is GenericArrayType) { - return serializersByType.computeIfAbsent(declaredType) { ArraySerializer(declaredType, this) } } else { throw NotSerializableException("Declared types of $declaredType are not supported.") } } + + /** + * Try and infer concrete types for any generics type variables for the actual class encountered, based on the declared + * type. + */ + // TODO: test GenericArrayType + private fun inferTypeVariables(actualClass: Class<*>?, declaredClass: Class<*>, declaredType: Type): Type? { + if (declaredType is ParameterizedType) { + return inferTypeVariables(actualClass, declaredClass, declaredType) + } else if (declaredType is Class<*>) { + // Nothing to infer, otherwise we'd have ParameterizedType + return actualClass + } else if (declaredType is GenericArrayType) { + val declaredComponent = declaredType.genericComponentType + return inferTypeVariables(actualClass?.componentType, declaredComponent.asClass()!!, declaredComponent)?.asArray() + } else return null + } + + /** + * Try and infer concrete types for any generics type variables for the actual class encountered, based on the declared + * type, which must be a [ParameterizedType]. + */ + private fun inferTypeVariables(actualClass: Class<*>?, declaredClass: Class<*>, declaredType: ParameterizedType): Type? { + if (actualClass == null || declaredClass == actualClass) { + return null + } else if (declaredClass.isAssignableFrom(actualClass)) { + return if (actualClass.typeParameters.isNotEmpty()) { + // The actual class can never have type variables resolved, due to the JVM's use of type erasure, so let's try and resolve them + // Search for declared type in the inheritance hierarchy and then see if that fills in all the variables + val implementationChain: List? = findPathToDeclared(actualClass, declaredType, mutableListOf()) + if (implementationChain != null) { + val start = implementationChain.last() + val rest = implementationChain.dropLast(1).drop(1) + val resolver = rest.reversed().fold(TypeResolver().where(start, declaredType)) { + resolved, chainEntry -> + val newResolved = resolved.resolveType(chainEntry) + TypeResolver().where(chainEntry, newResolved) + } + // The end type is a special case as it is a Class, so we need to fake up a ParameterizedType for it to get the TypeResolver to do anything. + val endType = DeserializedParameterizedType(actualClass, actualClass.typeParameters) + val resolvedType = resolver.resolveType(endType) + resolvedType + } else throw NotSerializableException("No inheritance path between actual $actualClass and declared $declaredType.") + } else actualClass + } else throw NotSerializableException("Found object of type $actualClass in a property expecting $declaredType") + } + + // Stop when reach declared type or return null if we don't find it. + private fun findPathToDeclared(startingType: Type, declaredType: Type, chain: MutableList): List? { + chain.add(startingType) + val startingClass = startingType.asClass() + if (startingClass == declaredType.asClass()) { + // We're done... + return chain + } + // Now explore potential options of superclass and all interfaces + val superClass = startingClass?.genericSuperclass + val superClassChain = if (superClass != null) { + val resolved = TypeResolver().where(startingClass.asParameterizedType(), startingType.asParameterizedType()).resolveType(superClass) + findPathToDeclared(resolved, declaredType, ArrayList(chain)) + } else null + if (superClassChain != null) return superClassChain + for (iface in startingClass?.genericInterfaces ?: emptyArray()) { + val resolved = TypeResolver().where(startingClass!!.asParameterizedType(), startingType.asParameterizedType()).resolveType(iface) + return findPathToDeclared(resolved, declaredType, ArrayList(chain)) ?: continue + } + return null + } + /** * Lookup and manufacture a serializer for the given AMQP type descriptor, assuming we also have the necessary types * contained in the [Schema]. @@ -93,7 +155,8 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) { } /** - * TODO: Add docs + * Register a custom serializer for any type that cannot be serialized or deserialized by the default serializer + * that expects to find getters and a constructor with a parameter for each property. */ fun register(customSerializer: CustomSerializer) { if (!serializersByDescriptor.containsKey(customSerializer.typeDescriptor)) { @@ -118,25 +181,10 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) { } } - private fun restrictedTypeForName(name: String): Type { - return if (name.endsWith("[]")) { - 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) - } - } - private fun processRestrictedType(typeNotation: RestrictedType) { serializersByDescriptor.computeIfAbsent(typeNotation.descriptor.name!!) { // TODO: class loader logic, and compare the schema. - val type = restrictedTypeForName(typeNotation.name) + val type = typeForName(typeNotation.name) get(null, type) } } @@ -144,63 +192,61 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) { private fun processCompositeType(typeNotation: CompositeType) { serializersByDescriptor.computeIfAbsent(typeNotation.descriptor.name!!) { // TODO: class loader logic, and compare the schema. - val clazz = Class.forName(typeNotation.name) - get(clazz, clazz) + val type = typeForName(typeNotation.name) + get(type.asClass() ?: throw NotSerializableException("Unable to build composite type for $type"), type) } } - private fun checkParameterisedTypesConcrete(actualTypeArguments: Array) { - for (type in actualTypeArguments) { - // Needs to be another parameterised type or a class, or any type. - if (type !is Class<*>) { - if (type is ParameterizedType) { - checkParameterisedTypesConcrete(type.actualTypeArguments) - } else if (type != AnyType) { - throw NotSerializableException("Declared parameterised types containing $type as a parameter are not supported.") + private fun makeClassSerializer(clazz: Class<*>, type: Type, declaredType: Type): AMQPSerializer { + return serializersByType.computeIfAbsent(type) { + if (isPrimitive(clazz)) { + AMQPPrimitiveSerializer(clazz) + } else { + findCustomSerializer(clazz, declaredType) ?: run { + if (type.isArray()) { + whitelisted(type.componentType()) + ArraySerializer(type, this) + } else if (clazz.kotlin.objectInstance != null) { + whitelisted(clazz) + SingletonSerializer(clazz, clazz.kotlin.objectInstance!!, this) + } else { + whitelisted(type) + ObjectSerializer(type, this) + } } } } } - private fun makeClassSerializer(clazz: Class<*>): AMQPSerializer { - return serializersByType.computeIfAbsent(clazz) { - if (isPrimitive(clazz)) { - AMQPPrimitiveSerializer(clazz) - } else { - findCustomSerializer(clazz) ?: { - if (clazz.isArray) { - whitelisted(clazz.componentType) - ArraySerializer(clazz, this) - } else { - whitelisted(clazz) - ObjectSerializer(clazz, this) - } - }() - } - } - } - - internal fun findCustomSerializer(clazz: Class<*>): AMQPSerializer? { + internal fun findCustomSerializer(clazz: Class<*>, declaredType: Type): AMQPSerializer? { + // e.g. Imagine if we provided a Map serializer this way, then it won't work if the declared type is AbstractMap, only Map. + // Otherwise it needs to inject additional schema for a RestrictedType source of the super type. Could be done, but do we need it? for (customSerializer in customSerializers) { if (customSerializer.isSerializerFor(clazz)) { - return customSerializer + val declaredSuperClass = declaredType.asClass()?.superclass + if (declaredSuperClass == null || !customSerializer.isSerializerFor(declaredSuperClass)) { + return customSerializer + } else { + // Make a subclass serializer for the subclass and return that... + @Suppress("UNCHECKED_CAST") + return CustomSerializer.SubClass(clazz, customSerializer as CustomSerializer) + } } } return null } - private fun whitelisted(clazz: Class<*>): Boolean { - if (whitelist.hasListed(clazz) || hasAnnotationInHierarchy(clazz)) { - return true - } else { - throw NotSerializableException("Class $clazz is not on the whitelist or annotated with @CordaSerializable.") + private fun whitelisted(type: Type) { + val clazz = type.asClass()!! + if (!whitelist.hasListed(clazz) && !hasAnnotationInHierarchy(clazz)) { + throw NotSerializableException("Class $type is not on the whitelist or annotated with @CordaSerializable.") } } // 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.interfaces.any { hasAnnotationInHierarchy(it) } || (type.superclass != null && hasAnnotationInHierarchy(type.superclass)) } @@ -211,9 +257,16 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) { } companion object { - fun isPrimitive(type: Type): Boolean = type is Class<*> && Primitives.wrap(type) in primitiveTypeNames + fun isPrimitive(type: Type): Boolean = primitiveTypeName(type) != null - fun primitiveTypeName(type: Type): String? = primitiveTypeNames[type as? Class<*>] + fun primitiveTypeName(type: Type): String? { + val clazz = type as? Class<*> ?: return null + return primitiveTypeNames[Primitives.unwrap(clazz)] + } + + fun primitiveType(type: String): Class<*>? { + return namesOfPrimitiveTypes[type] + } private val primitiveTypeNames: Map, String> = mapOf( Boolean::class.java to "boolean", @@ -221,7 +274,7 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) { UnsignedByte::class.java to "ubyte", Short::class.java to "short", UnsignedShort::class.java to "ushort", - Integer::class.java to "int", + Int::class.java to "int", UnsignedInteger::class.java to "uint", Long::class.java to "long", UnsignedLong::class.java to "ulong", @@ -233,9 +286,36 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) { Char::class.java to "char", Date::class.java to "timestamp", UUID::class.java to "uuid", - Binary::class.java to "binary", + ByteArray::class.java to "binary", String::class.java to "string", Symbol::class.java to "symbol") + + private val namesOfPrimitiveTypes: Map> = primitiveTypeNames.map { it.value to it.key }.toMap() + + fun nameForType(type: Type): String { + if (type is Class<*>) { + return primitiveTypeName(type) ?: if (type.isArray) "${nameForType(type.componentType)}[]" else type.name + } else if (type is ParameterizedType) { + return "${nameForType(type.rawType)}<${type.actualTypeArguments.joinToString { nameForType(it) }}>" + } else if (type is GenericArrayType) { + return "${nameForType(type.genericComponentType)}[]" + } else throw NotSerializableException("Unable to render type $type to a string.") + } + + private fun typeForName(name: String): Type { + return if (name.endsWith("[]")) { + val elementType = typeForName(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) + } + } } object AnyType : WildcardType { @@ -246,4 +326,3 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) { override fun toString(): String = "?" } } - diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/SingletonSerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/SingletonSerializer.kt new file mode 100644 index 0000000000..ac7fca8d78 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/SingletonSerializer.kt @@ -0,0 +1,32 @@ +package net.corda.core.serialization.amqp + +import org.apache.qpid.proton.codec.Data +import java.lang.reflect.Type + +/** + * A custom serializer that transports nothing on the wire (except a boolean "false", since AMQP does not support + * absolutely nothing, or null as a described type) when we have a singleton within the node that we just + * want converting back to that singleton instance on the receiving JVM. + */ +class SingletonSerializer(override val type: Class<*>, val singleton: Any, factory: SerializerFactory) : AMQPSerializer { + override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}" + private val interfaces = interfacesForSerialization(type) + + private fun generateProvides(): List = interfaces.map { it.typeName } + + internal val typeNotation: TypeNotation = RestrictedType(type.typeName, "Singleton", generateProvides(), "boolean", Descriptor(typeDescriptor, null), emptyList()) + + override fun writeClassInfo(output: SerializationOutput) { + output.writeTypeNotations(typeNotation) + } + + override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) { + data.withDescribed(typeNotation.descriptor) { + data.putBoolean(false) + } + } + + override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any { + return singleton + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/BigDecimalSerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/BigDecimalSerializer.kt new file mode 100644 index 0000000000..68d02d2350 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/BigDecimalSerializer.kt @@ -0,0 +1,11 @@ +package net.corda.core.serialization.amqp.custom + +import net.corda.core.serialization.amqp.CustomSerializer +import java.math.BigDecimal + +/** + * A serializer for [BigDecimal], utilising the string based helper. [BigDecimal] seems to have no import/export + * features that are precision independent other than via a string. The format of the string is discussed in the + * documentation for [BigDecimal.toString]. + */ +object BigDecimalSerializer : CustomSerializer.ToString(BigDecimal::class.java) \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/CurrencySerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/CurrencySerializer.kt new file mode 100644 index 0000000000..cdad5b2242 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/CurrencySerializer.kt @@ -0,0 +1,12 @@ +package net.corda.core.serialization.amqp.custom + +import net.corda.core.serialization.amqp.CustomSerializer +import java.util.* + +/** + * A custom serializer for the [Currency] class, utilizing the currency code string representation. + */ +object CurrencySerializer : CustomSerializer.ToString(Currency::class.java, + withInheritance = false, + maker = { Currency.getInstance(it) }, + unmaker = { it.currencyCode }) diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/InstantSerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/InstantSerializer.kt new file mode 100644 index 0000000000..aa0e32a927 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/InstantSerializer.kt @@ -0,0 +1,18 @@ +package net.corda.core.serialization.amqp.custom + +import net.corda.core.serialization.amqp.CustomSerializer +import net.corda.core.serialization.amqp.SerializerFactory +import java.time.Instant + +/** + * A serializer for [Instant] that uses a proxy object to write out the seconds since the epoch and the nanos. + */ +class InstantSerializer(factory: SerializerFactory) : CustomSerializer.Proxy(Instant::class.java, InstantProxy::class.java, factory) { + override val additionalSerializers: Iterable> = emptyList() + + override fun toProxy(obj: Instant): InstantProxy = InstantProxy(obj.epochSecond, obj.nano) + + override fun fromProxy(proxy: InstantProxy): Instant = Instant.ofEpochSecond(proxy.epochSeconds, proxy.nanos.toLong()) + + data class InstantProxy(val epochSeconds: Long, val nanos: Int) +} \ No newline at end of file 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 index 46536a1bed..747940eb4a 100644 --- 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 @@ -2,23 +2,25 @@ 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) { +/** + * A serializer that writes out a public key in X.509 format. + */ +object 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 val schemaForDocumentation = Schema(listOf(RestrictedType(type.toString(), "", listOf(type.toString()), SerializerFactory.primitiveTypeName(ByteArray::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) + output.writeObject(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) + val bits = input.readObject(obj, schema, ByteArray::class.java) as ByteArray + return Crypto.decodePublicKey(bits) } } \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/X500NameSerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/X500NameSerializer.kt new file mode 100644 index 0000000000..e45c45b5e9 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/X500NameSerializer.kt @@ -0,0 +1,25 @@ +package net.corda.core.serialization.amqp.custom + +import net.corda.core.serialization.amqp.* +import org.apache.qpid.proton.codec.Data +import org.bouncycastle.asn1.ASN1InputStream +import org.bouncycastle.asn1.x500.X500Name +import java.lang.reflect.Type + +/** + * Custom serializer for X500 names that utilizes their ASN.1 encoding on the wire. + */ +object X500NameSerializer : CustomSerializer.Implements(X500Name::class.java) { + override val additionalSerializers: Iterable> = emptyList() + + override val schemaForDocumentation = Schema(listOf(RestrictedType(type.toString(), "", listOf(type.toString()), SerializerFactory.primitiveTypeName(ByteArray::class.java)!!, descriptor, emptyList()))) + + override fun writeDescribedObject(obj: X500Name, data: Data, type: Type, output: SerializationOutput) { + output.writeObject(obj.encoded, data, clazz) + } + + override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): X500Name { + val binary = input.readObject(obj, schema, ByteArray::class.java) as ByteArray + return X500Name.getInstance(ASN1InputStream(binary).readObject()) + } +} \ No newline at end of file diff --git a/core/src/test/kotlin/net/corda/core/serialization/amqp/SerializationOutputTests.kt b/core/src/test/kotlin/net/corda/core/serialization/amqp/SerializationOutputTests.kt index 5896a3c292..52e7ca29a8 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/amqp/SerializationOutputTests.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/amqp/SerializationOutputTests.kt @@ -1,17 +1,26 @@ package net.corda.core.serialization.amqp +import net.corda.core.contracts.* +import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowException +import net.corda.core.identity.AbstractParty import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.EmptyWhitelist +import net.corda.core.serialization.KryoAMQPSerializer +import net.corda.core.utilities.CordaRuntimeException import net.corda.nodeapi.RPCException +import net.corda.testing.MEGA_CORP import net.corda.testing.MEGA_CORP_PUBKEY import org.apache.qpid.proton.codec.DecoderImpl import org.apache.qpid.proton.codec.EncoderImpl import org.junit.Test import java.io.IOException import java.io.NotSerializableException +import java.math.BigDecimal import java.nio.ByteBuffer +import java.time.Instant import java.util.* +import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue @@ -58,12 +67,38 @@ class SerializationOutputTests { @Suppress("AddVarianceModifier") data class GenericFoo(val bar: String, val pub: T) + data class ContainsGenericFoo(val contain: GenericFoo) + + data class NestedGenericFoo(val contain: GenericFoo) + + data class ContainsNestedGenericFoo(val contain: NestedGenericFoo) + data class TreeMapWrapper(val tree: TreeMap) data class NavigableMapWrapper(val tree: NavigableMap) data class SortedSetWrapper(val set: SortedSet) + open class InheritedGeneric(val foo: X) + + data class ExtendsGeneric(val bar: Int, val pub: String) : InheritedGeneric(pub) + + interface GenericInterface { + val pub: X + } + + data class ImplementsGenericString(val bar: Int, override val pub: String) : GenericInterface + + data class ImplementsGenericX(val bar: Int, override val pub: Y) : GenericInterface + + abstract class AbstractGenericX : GenericInterface + + data class InheritGenericX(val duke: Double, override val pub: A) : AbstractGenericX() + + data class CapturesGenericX(val foo: GenericInterface) + + object KotlinObject + class Mismatch(fred: Int) { val ginger: Int = fred @@ -85,7 +120,11 @@ class SerializationOutputTests { data class PolymorphicProperty(val foo: FooInterface?) - private fun serdes(obj: Any, factory: SerializerFactory = SerializerFactory(), freshDeserializationFactory: SerializerFactory = SerializerFactory(), expectedEqual: Boolean = true): Any { + private fun serdes(obj: Any, + factory: SerializerFactory = SerializerFactory(), + freshDeserializationFactory: SerializerFactory = SerializerFactory(), + expectedEqual: Boolean = true, + expectDeserializedEqual: Boolean = true): Any { val ser = SerializationOutput(factory) val bytes = ser.serialize(obj) @@ -103,6 +142,7 @@ class SerializationOutputTests { // Check that a vanilla AMQP decoder can deserialize without schema. val result = decoder.readObject() as Envelope assertNotNull(result) + println(result.schema) val des = DeserializationInput(freshDeserializationFactory) val desObj = des.deserialize(bytes) @@ -113,7 +153,7 @@ class SerializationOutputTests { val des2 = DeserializationInput(factory) val desObj2 = des2.deserialize(ser2.serialize(obj)) assertTrue(Objects.deepEquals(obj, desObj2) == expectedEqual) - assertTrue(Objects.deepEquals(desObj, desObj2)) + assertTrue(Objects.deepEquals(desObj, desObj2) == expectDeserializedEqual) // TODO: add some schema assertions to check correctly formed. return desObj2 @@ -155,7 +195,7 @@ class SerializationOutputTests { serdes(obj) } - @Test + @Test(expected = NotSerializableException::class) fun `test top level list array`() { val obj = arrayOf(listOf("Fred", "Ginger"), listOf("Rogers", "Hammerstein")) serdes(obj) @@ -197,12 +237,51 @@ class SerializationOutputTests { serdes(obj) } - @Test(expected = NotSerializableException::class) + @Test fun `test generic foo`() { val obj = GenericFoo("Fred", "Ginger") serdes(obj) } + @Test + fun `test generic foo as property`() { + val obj = ContainsGenericFoo(GenericFoo("Fred", "Ginger")) + serdes(obj) + } + + @Test + fun `test nested generic foo as property`() { + val obj = ContainsNestedGenericFoo(NestedGenericFoo(GenericFoo("Fred", "Ginger"))) + serdes(obj) + } + + // TODO: Generic interfaces / superclasses + + @Test + fun `test extends generic`() { + val obj = ExtendsGeneric(1, "Ginger") + serdes(obj) + } + + @Test + fun `test implements generic`() { + val obj = ImplementsGenericString(1, "Ginger") + serdes(obj) + } + + @Test + fun `test implements generic captured`() { + val obj = CapturesGenericX(ImplementsGenericX(1, "Ginger")) + serdes(obj) + } + + + @Test + fun `test inherits generic captured`() { + val obj = CapturesGenericX(InheritGenericX(1.0, "Ginger")) + serdes(obj) + } + @Test(expected = NotSerializableException::class) fun `test TreeMap`() { val obj = TreeMap() @@ -246,9 +325,9 @@ class SerializationOutputTests { @Test fun `test custom serializers on public key`() { val factory = SerializerFactory() - factory.register(net.corda.core.serialization.amqp.custom.PublicKeySerializer()) + factory.register(net.corda.core.serialization.amqp.custom.PublicKeySerializer) val factory2 = SerializerFactory() - factory2.register(net.corda.core.serialization.amqp.custom.PublicKeySerializer()) + factory2.register(net.corda.core.serialization.amqp.custom.PublicKeySerializer) val obj = MEGA_CORP_PUBKEY serdes(obj, factory, factory2) } @@ -267,8 +346,9 @@ class SerializationOutputTests { val factory2 = SerializerFactory() factory2.register(net.corda.core.serialization.amqp.custom.ThrowableSerializer(factory2)) - val obj = IllegalAccessException("message").fillInStackTrace() - serdes(obj, factory, factory2, false) + val t = IllegalAccessException("message").fillInStackTrace() + val desThrowable = serdes(t, factory, factory2, false) as Throwable + assertSerializedThrowableEquivalent(t, desThrowable) } @Test @@ -286,7 +366,19 @@ class SerializationOutputTests { throw IllegalStateException("Layer 2", t) } } catch(t: Throwable) { - serdes(t, factory, factory2, false) + val desThrowable = serdes(t, factory, factory2, false) as Throwable + assertSerializedThrowableEquivalent(t, desThrowable) + } + } + + fun assertSerializedThrowableEquivalent(t: Throwable, desThrowable: Throwable) { + assertTrue(desThrowable is CordaRuntimeException) // Since we don't handle the other case(s) yet + if (desThrowable is CordaRuntimeException) { + assertEquals("${t.javaClass.name}: ${t.message}", desThrowable.message) + assertTrue(desThrowable is CordaRuntimeException) + assertTrue(Objects.deepEquals(t.stackTrace, desThrowable.stackTrace)) + assertEquals(t.suppressed.size, desThrowable.suppressed.size) + t.suppressed.zip(desThrowable.suppressed).forEach { (before, after) -> assertSerializedThrowableEquivalent(before, after) } } } @@ -307,7 +399,8 @@ class SerializationOutputTests { throw e } } catch(t: Throwable) { - serdes(t, factory, factory2, false) + val desThrowable = serdes(t, factory, factory2, false) as Throwable + assertSerializedThrowableEquivalent(t, desThrowable) } } @@ -347,4 +440,88 @@ class SerializationOutputTests { serdes(obj) } + @Test + fun `test kotlin object`() { + serdes(KotlinObject) + } + + object FooContract : Contract { + override fun verify(tx: TransactionForContract) { + + } + + override val legalContractReference: SecureHash = SecureHash.Companion.sha256("FooContractLegal") + } + + class FooState : ContractState { + override val contract: Contract + get() = FooContract + override val participants: List + get() = emptyList() + } + + @Test + fun `test transaction state`() { + val state = TransactionState(FooState(), MEGA_CORP) + + val factory = SerializerFactory() + KryoAMQPSerializer.registerCustomSerializers(factory) + + val factory2 = SerializerFactory() + KryoAMQPSerializer.registerCustomSerializers(factory2) + + val desState = serdes(state, factory, factory2, expectedEqual = false, expectDeserializedEqual = false) + assertTrue(desState is TransactionState<*>) + assertTrue((desState as TransactionState<*>).data is FooState) + assertTrue(desState.notary == state.notary) + assertTrue(desState.encumbrance == state.encumbrance) + } + + @Test + fun `test currencies serialize`() { + val factory = SerializerFactory() + factory.register(net.corda.core.serialization.amqp.custom.CurrencySerializer) + + val factory2 = SerializerFactory() + factory2.register(net.corda.core.serialization.amqp.custom.CurrencySerializer) + + val obj = Currency.getInstance("USD") + serdes(obj, factory, factory2) + } + + @Test + fun `test big decimals serialize`() { + val factory = SerializerFactory() + factory.register(net.corda.core.serialization.amqp.custom.BigDecimalSerializer) + + val factory2 = SerializerFactory() + factory2.register(net.corda.core.serialization.amqp.custom.BigDecimalSerializer) + + val obj = BigDecimal("100000000000000000000000000000.00") + serdes(obj, factory, factory2) + } + + @Test + fun `test instants serialize`() { + val factory = SerializerFactory() + factory.register(net.corda.core.serialization.amqp.custom.InstantSerializer(factory)) + + val factory2 = SerializerFactory() + factory2.register(net.corda.core.serialization.amqp.custom.InstantSerializer(factory2)) + + val obj = Instant.now() + serdes(obj, factory, factory2) + } + + @Test + fun `test StateRef serialize`() { + val factory = SerializerFactory() + factory.register(net.corda.core.serialization.amqp.custom.InstantSerializer(factory)) + + val factory2 = SerializerFactory() + factory2.register(net.corda.core.serialization.amqp.custom.InstantSerializer(factory2)) + + val obj = StateRef(SecureHash.randomSHA256(), 0) + serdes(obj, factory, factory2) + } } \ No newline at end of file From 083b8265b5fa7bf5aa9d84599a2d80aa368b9cd5 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Thu, 29 Jun 2017 14:36:39 +0100 Subject: [PATCH 25/97] Restructure Crypto to use ASN.1 algorithm identifiers Remove use of Sun internal APIs and algorithm identifiers (which are incomplete and non-standard) in Crypto. Also eliminates uncertainty about which signature scheme is being used (and therefore iterating through several to find the correct one). --- .../corda/core/crypto/ContentSignerBuilder.kt | 2 +- .../kotlin/net/corda/core/crypto/Crypto.kt | 134 +++++++++--------- .../net/corda/core/crypto/SignatureScheme.kt | 9 +- 3 files changed, 77 insertions(+), 68 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/crypto/ContentSignerBuilder.kt b/core/src/main/kotlin/net/corda/core/crypto/ContentSignerBuilder.kt index ed3222bf18..cf679e8a7d 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/ContentSignerBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/ContentSignerBuilder.kt @@ -14,7 +14,7 @@ import java.security.Signature */ object ContentSignerBuilder { fun build(signatureScheme: SignatureScheme, privateKey: PrivateKey, provider: Provider?, random: SecureRandom? = null): ContentSigner { - val sigAlgId = AlgorithmIdentifier(signatureScheme.signatureOID) + val sigAlgId = signatureScheme.signatureOID val sig = Signature.getInstance(signatureScheme.signatureName, provider).apply { if (random != null) { initSign(privateKey, random) diff --git a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt index c508c9a176..b795c24ad6 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt @@ -1,24 +1,23 @@ package net.corda.core.crypto import net.corda.core.random63BitValue -import net.i2p.crypto.eddsa.* +import net.i2p.crypto.eddsa.EdDSAEngine +import net.i2p.crypto.eddsa.EdDSAPrivateKey +import net.i2p.crypto.eddsa.EdDSAPublicKey +import net.i2p.crypto.eddsa.EdDSASecurityProvider import net.i2p.crypto.eddsa.math.GroupElement import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec -import org.bouncycastle.asn1.ASN1EncodableVector -import org.bouncycastle.asn1.ASN1ObjectIdentifier -import org.bouncycastle.asn1.ASN1Sequence -import org.bouncycastle.asn1.DERSequence +import org.bouncycastle.asn1.* import org.bouncycastle.asn1.bc.BCObjectIdentifiers +import org.bouncycastle.asn1.nist.NISTObjectIdentifiers import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers import org.bouncycastle.asn1.pkcs.PrivateKeyInfo +import org.bouncycastle.asn1.sec.SECObjectIdentifiers import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.asn1.x509.BasicConstraints -import org.bouncycastle.asn1.x509.Extension -import org.bouncycastle.asn1.x509.NameConstraints -import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo +import org.bouncycastle.asn1.x509.* import org.bouncycastle.asn1.x9.X9ObjectIdentifiers import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.cert.X509v3CertificateBuilder @@ -45,13 +44,8 @@ import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PrivateKey import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PublicKey import org.bouncycastle.pqc.jcajce.spec.SPHINCS256KeyGenParameterSpec -import sun.security.pkcs.PKCS8Key -import sun.security.util.DerValue -import sun.security.x509.X509Key import java.math.BigInteger import java.security.* -import java.security.KeyFactory -import java.security.KeyPairGenerator import java.security.spec.InvalidKeySpecException import java.security.spec.PKCS8EncodedKeySpec import java.security.spec.X509EncodedKeySpec @@ -80,7 +74,8 @@ object Crypto { val RSA_SHA256 = SignatureScheme( 1, "RSA_SHA256", - PKCSObjectIdentifiers.id_RSASSA_PSS, + AlgorithmIdentifier(PKCSObjectIdentifiers.rsaEncryption, null), + emptyList(), BouncyCastleProvider.PROVIDER_NAME, "RSA", "SHA256WITHRSAANDMGF1", @@ -93,7 +88,8 @@ object Crypto { val ECDSA_SECP256K1_SHA256 = SignatureScheme( 2, "ECDSA_SECP256K1_SHA256", - X9ObjectIdentifiers.ecdsa_with_SHA256, + AlgorithmIdentifier(X9ObjectIdentifiers.ecdsa_with_SHA256, SECObjectIdentifiers.secp256k1), + listOf(AlgorithmIdentifier(X9ObjectIdentifiers.id_ecPublicKey, SECObjectIdentifiers.secp256k1)), BouncyCastleProvider.PROVIDER_NAME, "ECDSA", "SHA256withECDSA", @@ -106,7 +102,8 @@ object Crypto { val ECDSA_SECP256R1_SHA256 = SignatureScheme( 3, "ECDSA_SECP256R1_SHA256", - X9ObjectIdentifiers.ecdsa_with_SHA256, + AlgorithmIdentifier(X9ObjectIdentifiers.ecdsa_with_SHA256, SECObjectIdentifiers.secp256r1), + listOf(AlgorithmIdentifier(X9ObjectIdentifiers.id_ecPublicKey, SECObjectIdentifiers.secp256r1)), BouncyCastleProvider.PROVIDER_NAME, "ECDSA", "SHA256withECDSA", @@ -119,10 +116,12 @@ object Crypto { val EDDSA_ED25519_SHA512 = SignatureScheme( 4, "EDDSA_ED25519_SHA512", - ASN1ObjectIdentifier("1.3.101.112"), + // OID taken from https://tools.ietf.org/html/draft-ietf-curdle-pkix-00 + AlgorithmIdentifier(ASN1ObjectIdentifier("1.3.101.112"), null), + emptyList(), // We added EdDSA to bouncy castle for certificate signing. BouncyCastleProvider.PROVIDER_NAME, - EdDSAKey.KEY_ALGORITHM, + "1.3.101.112", EdDSAEngine.SIGNATURE_ALGORITHM, EdDSANamedCurveTable.getByName("ED25519"), 256, @@ -133,10 +132,12 @@ object Crypto { * SPHINCS-256 hash-based signature scheme. It provides 128bit security against post-quantum attackers * at the cost of larger key sizes and loss of compatibility. */ + val SHA512_256 = DLSequence(arrayOf(NISTObjectIdentifiers.id_sha512_256)) val SPHINCS256_SHA256 = SignatureScheme( 5, "SPHINCS-256_SHA512", - BCObjectIdentifiers.sphincs256_with_SHA512, + AlgorithmIdentifier(BCObjectIdentifiers.sphincs256_with_SHA512, DLSequence(arrayOf(ASN1Integer(0), SHA512_256))), + listOf(AlgorithmIdentifier(BCObjectIdentifiers.sphincs256, DLSequence(arrayOf(ASN1Integer(0), SHA512_256)))), "BCPQC", "SPHINCS256", "SHA512WITHSPHINCS256", @@ -161,9 +162,14 @@ object Crypto { SPHINCS256_SHA256 ).associateBy { it.schemeCodeName } - // We need to group signature schemes per algorithm, so to quickly identify them during decoding. - // Please note there are schemes with the same algorithm, e.g. EC (or ECDSA) keys are used for both ECDSA_SECP256K1_SHA256 and ECDSA_SECP256R1_SHA256. - private val algorithmGroups = supportedSignatureSchemes.values.groupBy { it.algorithmName } + /** + * Map of X.509 algorithm identifiers to signature schemes Corda recognises. See RFC 2459 for the format of + * algorithm identifiers. + */ + private val algorithmMap: Map + = (supportedSignatureSchemes.values.flatMap { scheme -> scheme.alternativeOIDs.map { oid -> Pair(oid, scheme) } } + + supportedSignatureSchemes.values.map { Pair(it.signatureOID, it) }) + .toMap() // This map is required to defend against users that forcibly call Security.addProvider / Security.removeProvider // that could cause unexpected and suspicious behaviour. @@ -175,7 +181,7 @@ object Crypto { private fun getBouncyCastleProvider() = BouncyCastleProvider().apply { putAll(EdDSASecurityProvider()) - addKeyInfoConverter(EDDSA_ED25519_SHA512.signatureOID, KeyInfoConverter(EDDSA_ED25519_SHA512)) + addKeyInfoConverter(EDDSA_ED25519_SHA512.signatureOID.algorithm, KeyInfoConverter(EDDSA_ED25519_SHA512)) } init { @@ -184,6 +190,21 @@ object Crypto { Security.addProvider(getBouncyCastleProvider()) } + /** + * Normalise an algorithm identifier by converting [DERNull] parameters into a Kotlin null value. + */ + private fun normaliseAlgorithmIdentifier(id: AlgorithmIdentifier): AlgorithmIdentifier { + return if (id.parameters is DERNull) { + AlgorithmIdentifier(id.algorithm, null) + } else { + id + } + } + + fun findSignatureScheme(algorithm: AlgorithmIdentifier): SignatureScheme { + return algorithmMap[normaliseAlgorithmIdentifier(algorithm)] ?: throw IllegalArgumentException("Unrecognised algorithm: ${algorithm}") + } + /** * Factory pattern to retrieve the corresponding [SignatureScheme] based on the type of the [String] input. * This function is usually called by key generators and verify signature functions. @@ -192,6 +213,7 @@ object Crypto { * @return a currently supported SignatureScheme. * @throws IllegalArgumentException if the requested signature scheme is not supported. */ + @Throws(IllegalArgumentException::class) fun findSignatureScheme(schemeCodeName: String): SignatureScheme = supportedSignatureSchemes[schemeCodeName] ?: throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $schemeCodeName") /** @@ -202,10 +224,24 @@ object Crypto { * @return a currently supported SignatureScheme. * @throws IllegalArgumentException if the requested key type is not supported. */ - fun findSignatureScheme(key: Key): SignatureScheme { - val algorithm = matchingAlgorithmName(key.algorithm) - algorithmGroups[algorithm]?.filter { validateKey(it, key) }?.firstOrNull { return it } - throw IllegalArgumentException("Unsupported key algorithm: ${key.algorithm} or invalid key format") + @Throws(IllegalArgumentException::class) + fun findSignatureScheme(key: PublicKey): SignatureScheme { + val keyInfo = SubjectPublicKeyInfo.getInstance(key.encoded) + return findSignatureScheme(keyInfo.algorithm) + } + + /** + * Retrieve the corresponding [SignatureScheme] based on the type of the input [Key]. + * This function is usually called when requiring to verify signatures and the signing schemes must be defined. + * For the supported signature schemes see [Crypto]. + * @param key either private or public. + * @return a currently supported SignatureScheme. + * @throws IllegalArgumentException if the requested key type is not supported. + */ + @Throws(IllegalArgumentException::class) + fun findSignatureScheme(key: PrivateKey): SignatureScheme { + val keyInfo = PrivateKeyInfo.getInstance(key.encoded) + return findSignatureScheme(keyInfo.privateKeyAlgorithm) } /** @@ -217,19 +253,9 @@ object Crypto { */ @Throws(IllegalArgumentException::class) fun decodePrivateKey(encodedKey: ByteArray): PrivateKey { - val algorithm = matchingAlgorithmName(PKCS8Key.parseKey(DerValue(encodedKey)).algorithm) - // There are cases where the same key algorithm is applied to different signature schemes. - // Currently, this occurs with ECDSA as it applies to either secp256K1 or secp256R1 curves. - // In such a case, we should try and identify which of the candidate schemes is the correct one so as - // to generate the appropriate key. - for (signatureScheme in algorithmGroups[algorithm]!!) { - try { - return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePrivate(PKCS8EncodedKeySpec(encodedKey)) - } catch (ikse: InvalidKeySpecException) { - // ignore it - only used to bypass the scheme that causes an exception, as it has the same name, but different params. - } - } - throw IllegalArgumentException("This private key cannot be decoded, please ensure it is PKCS8 encoded and the signature scheme is supported.") + val keyInfo = PrivateKeyInfo.getInstance(encodedKey) + val signatureScheme = findSignatureScheme(keyInfo.privateKeyAlgorithm) + return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePrivate(PKCS8EncodedKeySpec(encodedKey)) } /** @@ -270,19 +296,9 @@ object Crypto { */ @Throws(IllegalArgumentException::class) fun decodePublicKey(encodedKey: ByteArray): PublicKey { - val algorithm = matchingAlgorithmName(X509Key.parse(DerValue(encodedKey)).algorithm) - // There are cases where the same key algorithm is applied to different signature schemes. - // Currently, this occurs with ECDSA as it applies to either secp256K1 or secp256R1 curves. - // In such a case, we should try and identify which of the candidate schemes is the correct one so as - // to generate the appropriate key. - for (signatureScheme in algorithmGroups[algorithm]!!) { - try { - return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePublic(X509EncodedKeySpec(encodedKey)) - } catch (ikse: InvalidKeySpecException) { - // ignore it - only used to bypass the scheme that causes an exception, as it has the same name, but different params. - } - } - throw IllegalArgumentException("This public key cannot be decoded, please ensure it is X509 encoded and the signature scheme is supported.") + val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(encodedKey) + val signatureScheme = findSignatureScheme(subjectPublicKeyInfo.algorithm) + return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePublic(X509EncodedKeySpec(encodedKey)) } /** @@ -834,16 +850,6 @@ object Crypto { /** Check if the requested [SignatureScheme] is supported by the system. */ fun isSupportedSignatureScheme(signatureScheme: SignatureScheme): Boolean = supportedSignatureSchemes[signatureScheme.schemeCodeName] === signatureScheme - // map algorithm names returned from Keystore (or after encode/decode) to the supported algorithm names. - private fun matchingAlgorithmName(algorithm: String): String { - return when (algorithm) { - "EC" -> "ECDSA" - "SPHINCS-256" -> "SPHINCS256" - "1.3.6.1.4.1.22554.2.1" -> "SPHINCS256" // Unfortunately, PKCS8Key and X509Key parsing return the OID as the algorithm name and not SPHINCS256. - else -> algorithm - } - } - // validate a key, by checking its algorithmic params. private fun validateKey(signatureScheme: SignatureScheme, key: Key): Boolean { return when (key) { diff --git a/core/src/main/kotlin/net/corda/core/crypto/SignatureScheme.kt b/core/src/main/kotlin/net/corda/core/crypto/SignatureScheme.kt index 8f61f1b66d..c6f4c7a9ab 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/SignatureScheme.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/SignatureScheme.kt @@ -1,6 +1,6 @@ package net.corda.core.crypto -import org.bouncycastle.asn1.ASN1ObjectIdentifier +import org.bouncycastle.asn1.x509.AlgorithmIdentifier import java.security.Signature import java.security.spec.AlgorithmParameterSpec @@ -8,7 +8,9 @@ import java.security.spec.AlgorithmParameterSpec * This class is used to define a digital signature scheme. * @param schemeNumberID we assign a number ID for more efficient on-wire serialisation. Please ensure uniqueness between schemes. * @param schemeCodeName code name for this signature scheme (e.g. RSA_SHA256, ECDSA_SECP256K1_SHA256, ECDSA_SECP256R1_SHA256, EDDSA_ED25519_SHA512, SPHINCS-256_SHA512). - * @param signatureOID object identifier of the signature algorithm (e.g 1.3.101.112 for EdDSA) + * @param signatureOID ASN.1 algorithm identifier of the signature algorithm (e.g 1.3.101.112 for EdDSA) + * @param alternativeOIDs ASN.1 algorithm identifiers for keys of the signature, where we want to map multiple keys to + * the same signature scheme. * @param providerName the provider's name (e.g. "BC"). * @param algorithmName which signature algorithm is used (e.g. RSA, ECDSA. EdDSA, SPHINCS-256). * @param signatureName a signature-scheme name as required to create [Signature] objects (e.g. "SHA256withECDSA") @@ -20,7 +22,8 @@ import java.security.spec.AlgorithmParameterSpec data class SignatureScheme( val schemeNumberID: Int, val schemeCodeName: String, - val signatureOID: ASN1ObjectIdentifier, + val signatureOID: AlgorithmIdentifier, + val alternativeOIDs: List, val providerName: String, val algorithmName: String, val signatureName: String, From 00b272906abe8159a40d855a983f4e32601347df Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Thu, 29 Jun 2017 17:25:10 +0100 Subject: [PATCH 26/97] Decouple notary implementations from AbstractNode. Allow custom notaries to be provided via CorDapps. --- .../corda/core/node/services/NotaryService.kt | 78 +++++++++++++++++++ .../main/kotlin/net/corda/flows/NotaryFlow.kt | 51 ++---------- .../net/corda/node/internal/AbstractNode.kt | 57 +++++++------- .../kotlin/net/corda/node/internal/Node.kt | 18 ----- .../node/services/api/ServiceHubInternal.kt | 4 + .../BFTNonValidatingNotaryService.kt | 72 +++++++++-------- .../node/services/transactions/BFTSMaRt.kt | 12 ++- .../InMemoryUniquenessProvider.kt | 36 --------- .../transactions/NonValidatingNotaryFlow.kt | 7 +- .../services/transactions/NotaryService.kt | 15 ---- .../RaftNonValidatingNotaryService.kt | 24 ++++-- .../transactions/RaftUniquenessProvider.kt | 53 ++++++++----- .../RaftValidatingNotaryService.kt | 22 ++++-- .../transactions/SimpleNotaryService.kt | 20 +++-- .../transactions/ValidatingNotaryFlow.kt | 8 +- .../transactions/ValidatingNotaryService.kt | 20 +++-- .../node/services/MockServiceHubInternal.kt | 7 +- .../InMemoryUniquenessProviderTests.kt | 36 --------- .../kotlin/net/corda/testing/node/MockNode.kt | 3 - 19 files changed, 270 insertions(+), 273 deletions(-) create mode 100644 core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt delete mode 100644 node/src/main/kotlin/net/corda/node/services/transactions/InMemoryUniquenessProvider.kt delete mode 100644 node/src/main/kotlin/net/corda/node/services/transactions/NotaryService.kt delete mode 100644 node/src/test/kotlin/net/corda/node/services/transactions/InMemoryUniquenessProviderTests.kt diff --git a/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt b/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt new file mode 100644 index 0000000000..aa3def742c --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt @@ -0,0 +1,78 @@ +package net.corda.core.node.services + +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TimeWindow +import net.corda.core.crypto.DigitalSignature +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.SignedData +import net.corda.core.flows.FlowLogic +import net.corda.core.identity.Party +import net.corda.core.node.ServiceHub +import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.core.serialization.serialize +import net.corda.core.utilities.loggerFor +import net.corda.flows.NotaryError +import net.corda.flows.NotaryException +import org.slf4j.Logger + +abstract class NotaryService : SingletonSerializeAsToken() { + abstract val services: ServiceHub + + abstract fun start() + abstract fun stop() + + /** + * Produces a notary service flow which has the corresponding sends and receives as [NotaryFlow.Client]. + * The first parameter is the client [Party] making the request and the second is the platform version + * of the client's node. Use this version parameter to provide backwards compatibility if the notary flow protocol + * changes. + */ + abstract fun createServiceFlow(otherParty: Party, platformVersion: Int): FlowLogic +} + +/** + * A base notary service implementation that provides functionality for cases where a signature by a single member + * of the cluster is sufficient for transaction notarisation. For example, a single-node or a Raft notary. + */ +abstract class TrustedAuthorityNotaryService : NotaryService() { + protected open val log: Logger = loggerFor() + + // TODO: specify the valid time window in config, and convert TimeWindowChecker to a utility method + protected abstract val timeWindowChecker: TimeWindowChecker + protected abstract val uniquenessProvider: UniquenessProvider + + fun validateTimeWindow(t: TimeWindow?) { + if (t != null && !timeWindowChecker.isValid(t)) + throw NotaryException(NotaryError.TimeWindowInvalid) + } + + /** + * A NotaryException is thrown if any of the states have been consumed by a different transaction. Note that + * this method does not throw an exception when input states are present multiple times within the transaction. + */ + fun commitInputStates(inputs: List, txId: SecureHash, caller: Party) { + try { + uniquenessProvider.commit(inputs, txId, caller) + } catch (e: UniquenessException) { + val conflicts = inputs.filterIndexed { i, stateRef -> + val consumingTx = e.error.stateHistory[stateRef] + consumingTx != null && consumingTx != UniquenessProvider.ConsumingTx(txId, i, caller) + } + if (conflicts.isNotEmpty()) { + // TODO: Create a new UniquenessException that only contains the conflicts filtered above. + log.warn("Notary conflicts for $txId: $conflicts") + throw notaryException(txId, e) + } + } + } + + private fun notaryException(txId: SecureHash, e: UniquenessException): NotaryException { + val conflictData = e.error.serialize() + val signedConflict = SignedData(conflictData, sign(conflictData.bytes)) + return NotaryException(NotaryError.Conflict(txId, signedConflict)) + } + + fun sign(bits: ByteArray): DigitalSignature.WithKey { + return services.keyManagementService.sign(bits, services.notaryIdentityKey) + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/flows/NotaryFlow.kt b/core/src/main/kotlin/net/corda/flows/NotaryFlow.kt index a12770ca1f..579f9c8125 100644 --- a/core/src/main/kotlin/net/corda/flows/NotaryFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/NotaryFlow.kt @@ -11,11 +11,8 @@ 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.TimeWindowChecker -import net.corda.core.node.services.UniquenessException -import net.corda.core.node.services.UniquenessProvider +import net.corda.core.node.services.* import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.serialize import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.unwrap @@ -97,14 +94,13 @@ object NotaryFlow { * 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 timeWindowChecker: TimeWindowChecker, - val uniquenessProvider: UniquenessProvider) : FlowLogic() { + abstract class Service(val otherSide: Party, val service: TrustedAuthorityNotaryService) : FlowLogic() { + @Suspendable override fun call(): Void? { val (id, inputs, timeWindow) = receiveAndVerifyTx() - validateTimeWindow(timeWindow) - commitInputStates(inputs, id) + service.validateTimeWindow(timeWindow) + service.commitInputStates(inputs, id, otherSide) signAndSendResponse(id) return null } @@ -118,44 +114,9 @@ object NotaryFlow { @Suspendable private fun signAndSendResponse(txId: SecureHash) { - val signature = sign(txId.bytes) + val signature = service.sign(txId.bytes) send(otherSide, listOf(signature)) } - - private fun validateTimeWindow(t: TimeWindow?) { - if (t != null && !timeWindowChecker.isValid(t)) - throw NotaryException(NotaryError.TimeWindowInvalid) - } - - /** - * A NotaryException is thrown if any of the states have been consumed by a different transaction. Note that - * this method does not throw an exception when input states are present multiple times within the transaction. - */ - private fun commitInputStates(inputs: List, txId: SecureHash) { - try { - uniquenessProvider.commit(inputs, txId, otherSide) - } catch (e: UniquenessException) { - val conflicts = inputs.filterIndexed { i, stateRef -> - val consumingTx = e.error.stateHistory[stateRef] - consumingTx != null && consumingTx != UniquenessProvider.ConsumingTx(txId, i, otherSide) - } - if (conflicts.isNotEmpty()) { - // TODO: Create a new UniquenessException that only contains the conflicts filtered above. - logger.warn("Notary conflicts for $txId: $conflicts") - throw notaryException(txId, e) - } - } - } - - private fun sign(bits: ByteArray): DigitalSignature.WithKey { - return serviceHub.keyManagementService.sign(bits, serviceHub.notaryIdentityKey) - } - - private fun notaryException(txId: SecureHash, e: UniquenessException): NotaryException { - val conflictData = e.error.serialize() - val signedConflict = SignedData(conflictData, sign(conflictData.bytes)) - return NotaryException(NotaryError.Conflict(txId, signedConflict)) - } } } diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 1f28e00a0b..18fef52f56 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -20,6 +20,7 @@ import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.* import net.corda.core.node.services.* import net.corda.core.node.services.NetworkMapCache.MapChange +import net.corda.core.node.services.NotaryService import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.deserialize @@ -59,7 +60,6 @@ import net.corda.node.utilities.AddOrRemove.ADD import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction -import net.corda.nodeapi.ArtemisMessagingComponent import org.apache.activemq.artemis.utils.ReusableLatch import org.bouncycastle.asn1.x500.X500Name import org.jetbrains.exposed.sql.Database @@ -133,7 +133,9 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, override val myInfo: NodeInfo get() = info override val schemaService: SchemaService get() = schemas override val transactionVerifierService: TransactionVerifierService get() = txVerifierService - override val auditService: AuditService get() = auditService + override val auditService: AuditService get() = this@AbstractNode.auditService + override val database: Database get() = this@AbstractNode.database + override val configuration: NodeConfiguration get() = this@AbstractNode.configuration override fun cordaService(type: Class): T { require(type.isAnnotationPresent(CordaService::class.java)) { "${type.name} is not a Corda service" } @@ -329,10 +331,19 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, } cordappServices.putInstance(serviceClass, service) smm.tokenizableServices += service + + if (service is NotaryService) handleCustomNotaryService(service) + log.info("Installed ${serviceClass.name} Corda service") return service } + private fun handleCustomNotaryService(service: NotaryService) { + runOnStop += service::stop + service.start() + installCoreFlow(NotaryFlow.Client::class, { party: Party, version: Int -> service.createServiceFlow(party, version) }) + } + private inline fun Class<*>.requireAnnotation(): A { return requireNotNull(getDeclaredAnnotation(A::class.java)) { "$name needs to be annotated with ${A::class.java.name}" } } @@ -624,7 +635,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val notaryServiceType = serviceTypes.singleOrNull { it.isNotary() } if (notaryServiceType != null) { - makeNotaryService(notaryServiceType, tokenizableServices) + makeCoreNotaryService(notaryServiceType, tokenizableServices) } } @@ -685,35 +696,27 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, inNodeNetworkMapService = PersistentNetworkMapService(services, configuration.minimumPlatformVersion) } - open protected fun makeNotaryService(type: ServiceType, tokenizableServices: MutableList) { - val timeWindowChecker = TimeWindowChecker(platformClock, 30.seconds) - val uniquenessProvider = makeUniquenessProvider(type) - tokenizableServices.add(uniquenessProvider) - - val notaryService = when (type) { - SimpleNotaryService.type -> SimpleNotaryService(timeWindowChecker, uniquenessProvider) - ValidatingNotaryService.type -> ValidatingNotaryService(timeWindowChecker, uniquenessProvider) - RaftNonValidatingNotaryService.type -> RaftNonValidatingNotaryService(timeWindowChecker, uniquenessProvider as RaftUniquenessProvider) - RaftValidatingNotaryService.type -> RaftValidatingNotaryService(timeWindowChecker, uniquenessProvider as RaftUniquenessProvider) - BFTNonValidatingNotaryService.type -> with(configuration) { - val replicaId = bftReplicaId ?: throw IllegalArgumentException("bftReplicaId value must be specified in the configuration") - BFTSMaRtConfig(notaryClusterAddresses).use { config -> - BFTNonValidatingNotaryService(config, services, timeWindowChecker, replicaId, database).also { - tokenizableServices += it.client - runOnStop += it::dispose - } - } - } + open protected fun makeCoreNotaryService(type: ServiceType, tokenizableServices: MutableList) { + val service: NotaryService = when (type) { + SimpleNotaryService.type -> SimpleNotaryService(services) + ValidatingNotaryService.type -> ValidatingNotaryService(services) + RaftNonValidatingNotaryService.type -> RaftNonValidatingNotaryService(services) + RaftValidatingNotaryService.type -> RaftValidatingNotaryService(services) + BFTNonValidatingNotaryService.type -> BFTNonValidatingNotaryService(services) else -> { - throw IllegalArgumentException("Notary type ${type.id} is not handled by makeNotaryService.") + log.info("Notary type ${type.id} does not match any built-in notary types. " + + "It is expected to be loaded via a CorDapp") + return } } - - installCoreFlow(NotaryFlow.Client::class, notaryService.serviceFlowFactory) + service.apply { + tokenizableServices.add(this) + runOnStop += this::stop + start() + } + installCoreFlow(NotaryFlow.Client::class, { party: Party, version: Int -> service.createServiceFlow(party, version) }) } - protected abstract fun makeUniquenessProvider(type: ServiceType): UniquenessProvider - protected open fun makeIdentityService(trustRoot: X509Certificate, clientCa: CertificateAndKeyPair?, legalIdentity: PartyAndCertificate): IdentityService { diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index 347f35edd2..993fe6391b 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -11,8 +11,6 @@ import net.corda.core.minutes import net.corda.core.node.ServiceHub import net.corda.core.node.VersionInfo import net.corda.core.node.services.ServiceInfo -import net.corda.core.node.services.ServiceType -import net.corda.core.node.services.UniquenessProvider import net.corda.core.seconds import net.corda.core.success import net.corda.core.utilities.loggerFor @@ -26,10 +24,6 @@ import net.corda.node.services.messaging.ArtemisMessagingServer.Companion.ipDete import net.corda.node.services.messaging.ArtemisMessagingServer.Companion.ipDetectResponseProperty import net.corda.node.services.messaging.MessagingService import net.corda.node.services.messaging.NodeMessagingClient -import net.corda.node.services.transactions.PersistentUniquenessProvider -import net.corda.node.services.transactions.RaftNonValidatingNotaryService -import net.corda.node.services.transactions.RaftUniquenessProvider -import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.node.utilities.AddressUtils import net.corda.node.utilities.AffinityExecutor import net.corda.nodeapi.ArtemisMessagingComponent @@ -263,18 +257,6 @@ open class Node(override val configuration: FullNodeConfiguration, return networkMapConnection.flatMap { super.registerWithNetworkMap() } } - override fun makeUniquenessProvider(type: ServiceType): UniquenessProvider { - return when (type) { - RaftValidatingNotaryService.type, RaftNonValidatingNotaryService.type -> with(configuration) { - val provider = RaftUniquenessProvider(baseDirectory, notaryNodeAddress!!, notaryClusterAddresses, database, configuration) - provider.start() - runOnStop += provider::stop - provider - } - else -> PersistentUniquenessProvider() - } - } - override fun myAddresses(): List { val address = network.myAddress as ArtemisMessagingComponent.ArtemisPeerAddress return listOf(address.hostAndPort) diff --git a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt index abcda505f4..65c48f39b6 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt @@ -15,9 +15,11 @@ import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.loggerFor import net.corda.node.internal.InitiatedFlowFactory +import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.MessagingService import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl import net.corda.node.services.statemachine.FlowStateMachineImpl +import org.jetbrains.exposed.sql.Database interface NetworkMapCacheInternal : NetworkMapCache { /** @@ -68,6 +70,8 @@ abstract class ServiceHubInternal : PluginServiceHub { abstract val auditService: AuditService abstract val rpcFlows: List>> abstract val networkService: MessagingService + abstract val database: Database + abstract val configuration: NodeConfiguration /** * Given a list of [SignedTransaction]s, writes them to the given storage for validated transactions and then diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt index 4dd60e71a9..a64cfaaa9d 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt @@ -6,6 +6,7 @@ import net.corda.core.crypto.DigitalSignature import net.corda.core.flows.FlowLogic import net.corda.core.getOrThrow import net.corda.core.identity.Party +import net.corda.core.node.services.NotaryService import net.corda.core.node.services.TimeWindowChecker import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize @@ -15,7 +16,6 @@ import net.corda.core.utilities.loggerFor import net.corda.core.utilities.unwrap import net.corda.flows.NotaryException import net.corda.node.services.api.ServiceHubInternal -import org.jetbrains.exposed.sql.Database import kotlin.concurrent.thread /** @@ -23,40 +23,42 @@ import kotlin.concurrent.thread * * A transaction is notarised when the consensus is reached by the cluster on its uniqueness, and time-window validity. */ -class BFTNonValidatingNotaryService(config: BFTSMaRtConfig, - services: ServiceHubInternal, - timeWindowChecker: TimeWindowChecker, - replicaId: Int, - db: Database) : NotaryService { - val client = BFTSMaRt.Client(config, replicaId) // (Ab)use replicaId for clientId. - private val replicaHolder = SettableFuture.create() - - init { - // Replica startup must be in parallel with other replicas, otherwise the constructor may not return: - val configHandle = config.handle() - thread(name = "BFT SMaRt replica $replicaId init", isDaemon = true) { - configHandle.use { - replicaHolder.set(Replica(it, replicaId, db, "bft_smart_notary_committed_states", services, timeWindowChecker)) - log.info("BFT SMaRt replica $replicaId is running.") - } - } - } - - fun dispose() { - replicaHolder.getOrThrow().dispose() - client.dispose() - } - +class BFTNonValidatingNotaryService(override val services: ServiceHubInternal) : NotaryService() { companion object { val type = SimpleNotaryService.type.getSubType("bft") private val log = loggerFor() } - override val serviceFlowFactory: (Party, Int) -> FlowLogic = { otherParty, _ -> - ServiceFlow(otherParty, client) + private val client: BFTSMaRt.Client + private val replicaHolder = SettableFuture.create() + + init { + val replicaId = services.configuration.bftReplicaId ?: throw IllegalArgumentException("bftReplicaId value must be specified in the configuration") + val config = BFTSMaRtConfig(services.configuration.notaryClusterAddresses) + + client = config.use { + val configHandle = config.handle() + // Replica startup must be in parallel with other replicas, otherwise the constructor may not return: + thread(name = "BFT SMaRt replica $replicaId init", isDaemon = true) { + configHandle.use { + val timeWindowChecker = TimeWindowChecker(services.clock) + val replica = Replica(it, replicaId, "bft_smart_notary_committed_states", services, timeWindowChecker) + replicaHolder.set(replica) + log.info("BFT SMaRt replica $replicaId is running.") + } + } + + BFTSMaRt.Client(it, replicaId) + } } - private class ServiceFlow(val otherSide: Party, val client: BFTSMaRt.Client) : FlowLogic() { + fun commitTransaction(tx: Any, otherSide: Party) = client.commitTransaction(tx, otherSide) + + override fun createServiceFlow(otherParty: Party, platformVersion: Int): FlowLogic { + return ServiceFlow(otherParty, this) + } + + private class ServiceFlow(val otherSide: Party, val service: BFTNonValidatingNotaryService) : FlowLogic() { @Suspendable override fun call(): Void? { val stx = receive(otherSide).unwrap { it } @@ -66,7 +68,7 @@ class BFTNonValidatingNotaryService(config: BFTSMaRtConfig, } private fun commit(stx: FilteredTransaction): List { - val response = client.commitTransaction(stx, otherSide) + val response = service.commitTransaction(stx, otherSide) when (response) { is BFTSMaRt.ClusterResponse.Error -> throw NotaryException(response.error) is BFTSMaRt.ClusterResponse.Signatures -> { @@ -79,10 +81,9 @@ class BFTNonValidatingNotaryService(config: BFTSMaRtConfig, private class Replica(config: BFTSMaRtConfig, replicaId: Int, - db: Database, tableName: String, services: ServiceHubInternal, - timeWindowChecker: TimeWindowChecker) : BFTSMaRt.Replica(config, replicaId, db, tableName, services, timeWindowChecker) { + timeWindowChecker: TimeWindowChecker) : BFTSMaRt.Replica(config, replicaId, tableName, services, timeWindowChecker) { override fun executeCommand(command: ByteArray): ByteArray { val request = command.deserialize() @@ -107,5 +108,14 @@ class BFTNonValidatingNotaryService(config: BFTSMaRtConfig, BFTSMaRt.ReplicaResponse.Error(e.error) } } + + } + + override fun start() { + } + + override fun stop() { + replicaHolder.getOrThrow().dispose() + client.dispose() } } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt b/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt index 2a13f0cab4..d95f768a4a 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt @@ -37,7 +37,6 @@ import net.corda.node.services.transactions.BFTSMaRt.Client import net.corda.node.services.transactions.BFTSMaRt.Replica import net.corda.node.utilities.JDBCHashMap import net.corda.node.utilities.transaction -import org.jetbrains.exposed.sql.Database import java.nio.file.Path import java.util.* @@ -170,7 +169,6 @@ object BFTSMaRt { */ abstract class Replica(config: BFTSMaRtConfig, replicaId: Int, - private val db: Database, tableName: String, private val services: ServiceHubInternal, private val timeWindowChecker: TimeWindowChecker) : DefaultRecoverable() { @@ -180,7 +178,7 @@ object BFTSMaRt { // TODO: Use Requery with proper DB schema instead of JDBCHashMap. // Must be initialised before ServiceReplica is started - private val commitLog = db.transaction { JDBCHashMap(tableName) } + private val commitLog = services.database.transaction { JDBCHashMap(tableName) } @Suppress("LeakingThis") private val replica = CordaServiceReplica(replicaId, config.path, this) @@ -205,7 +203,7 @@ object BFTSMaRt { protected fun commitInputStates(states: List, txId: SecureHash, callerIdentity: Party) { log.debug { "Attempting to commit inputs for transaction: $txId" } val conflicts = mutableMapOf() - db.transaction { + services.database.transaction { states.forEach { state -> commitLog[state]?.let { conflicts[state] = it } } @@ -231,7 +229,7 @@ object BFTSMaRt { } protected fun sign(bytes: ByteArray): DigitalSignature.WithKey { - return db.transaction { services.keyManagementService.sign(bytes, services.notaryIdentityKey) } + return services.database.transaction { services.keyManagementService.sign(bytes, services.notaryIdentityKey) } } // TODO: @@ -240,7 +238,7 @@ object BFTSMaRt { override fun getSnapshot(): ByteArray { // LinkedHashMap for deterministic serialisation val m = LinkedHashMap() - db.transaction { + services.database.transaction { commitLog.forEach { m[it.key] = it.value } } return m.serialize().bytes @@ -248,7 +246,7 @@ object BFTSMaRt { override fun installSnapshot(bytes: ByteArray) { val m = bytes.deserialize>() - db.transaction { + services.database.transaction { commitLog.clear() commitLog.putAll(m) } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/InMemoryUniquenessProvider.kt b/node/src/main/kotlin/net/corda/node/services/transactions/InMemoryUniquenessProvider.kt deleted file mode 100644 index 17021d1c1b..0000000000 --- a/node/src/main/kotlin/net/corda/node/services/transactions/InMemoryUniquenessProvider.kt +++ /dev/null @@ -1,36 +0,0 @@ -package net.corda.node.services.transactions - -import net.corda.core.ThreadBox -import net.corda.core.contracts.StateRef -import net.corda.core.identity.Party -import net.corda.core.crypto.SecureHash -import net.corda.core.node.services.UniquenessException -import net.corda.core.node.services.UniquenessProvider -import java.util.* -import javax.annotation.concurrent.ThreadSafe - -/** A dummy Uniqueness provider that stores the whole history of consumed states in memory */ -@ThreadSafe -class InMemoryUniquenessProvider : UniquenessProvider { - /** For each input state store the consuming transaction information */ - private val committedStates = ThreadBox(HashMap()) - - override fun commit(states: List, txId: SecureHash, callerIdentity: Party) { - committedStates.locked { - val conflictingStates = LinkedHashMap() - for (inputState in states) { - val consumingTx = get(inputState) - if (consumingTx != null) conflictingStates[inputState] = consumingTx - } - if (conflictingStates.isNotEmpty()) { - val conflict = UniquenessProvider.Conflict(conflictingStates) - throw UniquenessException(conflict) - } else { - states.forEachIndexed { i, stateRef -> - put(stateRef, UniquenessProvider.ConsumingTx(txId, i, callerIdentity)) - } - } - - } - } -} diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt b/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt index 470ad66a8e..354ef7799d 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt @@ -2,16 +2,13 @@ package net.corda.node.services.transactions import co.paralleluniverse.fibers.Suspendable import net.corda.core.identity.Party -import net.corda.core.node.services.TimeWindowChecker -import net.corda.core.node.services.UniquenessProvider +import net.corda.core.node.services.TrustedAuthorityNotaryService import net.corda.core.transactions.FilteredTransaction import net.corda.core.utilities.unwrap import net.corda.flows.NotaryFlow import net.corda.flows.TransactionParts -class NonValidatingNotaryFlow(otherSide: Party, - timeWindowChecker: TimeWindowChecker, - uniquenessProvider: UniquenessProvider) : NotaryFlow.Service(otherSide, timeWindowChecker, uniquenessProvider) { +class NonValidatingNotaryFlow(otherSide: Party, service: TrustedAuthorityNotaryService) : NotaryFlow.Service(otherSide, service) { /** * The received transaction is not checked for contract-validity, as that would require fully * resolving it into a [TransactionForVerification], for which the caller would have to reveal the whole transaction diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/NotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/NotaryService.kt deleted file mode 100644 index 9abef4eb50..0000000000 --- a/node/src/main/kotlin/net/corda/node/services/transactions/NotaryService.kt +++ /dev/null @@ -1,15 +0,0 @@ -package net.corda.node.services.transactions - -import net.corda.core.flows.FlowLogic -import net.corda.core.identity.Party - -interface NotaryService { - - /** - * Factory for producing notary service flows which have the corresponding sends and receives as NotaryFlow.Client. - * The first parameter is the client [Party] making the request and the second is the platform version - * of the client's node. Use this version parameter to provide backwards compatibility if the notary flow protocol - * changes. - */ - val serviceFlowFactory: (Party, Int) -> FlowLogic -} diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt index 614dfdeb36..05bcabe172 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt @@ -1,17 +1,29 @@ package net.corda.node.services.transactions -import net.corda.core.flows.FlowLogic import net.corda.core.identity.Party +import net.corda.core.node.services.TrustedAuthorityNotaryService import net.corda.core.node.services.TimeWindowChecker +import net.corda.flows.NotaryFlow +import net.corda.node.services.api.ServiceHubInternal /** A non-validating notary service operated by a group of mutually trusting parties, uses the Raft algorithm to achieve consensus. */ -class RaftNonValidatingNotaryService(val timeWindowChecker: TimeWindowChecker, - val uniquenessProvider: RaftUniquenessProvider) : NotaryService { +class RaftNonValidatingNotaryService(override val services: ServiceHubInternal) : TrustedAuthorityNotaryService() { companion object { val type = SimpleNotaryService.type.getSubType("raft") } - override val serviceFlowFactory: (Party, Int) -> FlowLogic = { otherParty, _ -> - NonValidatingNotaryFlow(otherParty, timeWindowChecker, uniquenessProvider) + override val timeWindowChecker: TimeWindowChecker = TimeWindowChecker(services.clock) + override val uniquenessProvider: RaftUniquenessProvider = RaftUniquenessProvider(services) + + override fun createServiceFlow(otherParty: Party, platformVersion: Int): NotaryFlow.Service { + return NonValidatingNotaryFlow(otherParty, this) } -} + + override fun start() { + uniquenessProvider.start() + } + + override fun stop() { + uniquenessProvider.stop() + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt index 87a09a22e3..e0f514cc32 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt @@ -23,6 +23,7 @@ import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.utilities.loggerFor +import net.corda.node.services.api.ServiceHubInternal import net.corda.nodeapi.config.SSLConfiguration import org.jetbrains.exposed.sql.Database import java.nio.file.Path @@ -36,27 +37,29 @@ import javax.annotation.concurrent.ThreadSafe * The uniqueness provider maintains both a Copycat cluster node (server) and a client through which it can submit * requests to the cluster. In Copycat, a client request is first sent to the server it's connected to and then redirected * to the cluster leader to be actioned. - * - * @param storagePath Directory storing the Raft log and state machine snapshots - * @param myAddress Address of the Copycat node run by this Corda node - * @param clusterAddresses List of node addresses in the existing Copycat cluster. At least one active node must be - * provided to join the cluster. If empty, a new cluster will be bootstrapped. - * @param db The database to store the state machine state in - * @param config SSL configuration */ @ThreadSafe -class RaftUniquenessProvider( - val storagePath: Path, - val myAddress: HostAndPort, - val clusterAddresses: List, - val db: Database, - val config: SSLConfiguration -) : UniquenessProvider, SingletonSerializeAsToken() { +class RaftUniquenessProvider(services: ServiceHubInternal) : UniquenessProvider, SingletonSerializeAsToken() { companion object { private val log = loggerFor() private val DB_TABLE_NAME = "notary_committed_states" } + /** Directory storing the Raft log and state machine snapshots */ + private val storagePath: Path = services.configuration.baseDirectory + /** Address of the Copycat node run by this Corda node */ + private val myAddress: HostAndPort = services.configuration.notaryNodeAddress + ?: throw IllegalArgumentException("notaryNodeAddress must be specified in configuration") + /** + * List of node addresses in the existing Copycat cluster. At least one active node must be + * provided to join the cluster. If empty, a new cluster will be bootstrapped. + */ + private val clusterAddresses: List = services.configuration.notaryClusterAddresses + /** The database to store the state machine state in */ + private val db: Database = services.database + /** SSL configuration */ + private val transportConfiguration: SSLConfiguration = services.configuration + private lateinit var _clientFuture: CompletableFuture private lateinit var server: CopycatServer /** @@ -71,13 +74,21 @@ class RaftUniquenessProvider( val stateMachineFactory = { DistributedImmutableMap(db, DB_TABLE_NAME) } val address = Address(myAddress.host, myAddress.port) val storage = buildStorage(storagePath) - val transport = buildTransport(config) + val transport = buildTransport(transportConfiguration) val serializer = Serializer().apply { // Add serializers so Catalyst doesn't attempt to fall back on Java serialization for these types, which is disabled process-wide: register(DistributedImmutableMap.Commands.PutAll::class.java) { object : TypeSerializer> { - override fun write(obj: DistributedImmutableMap.Commands.PutAll<*, *>, buffer: BufferOutput>, serializer: Serializer) = writeMap(obj.entries, buffer, serializer) - override fun read(type: Class>, buffer: BufferInput>, serializer: Serializer) = DistributedImmutableMap.Commands.PutAll(readMap(buffer, serializer)) + override fun write(obj: DistributedImmutableMap.Commands.PutAll<*, *>, + buffer: BufferOutput>, + serializer: Serializer) { + writeMap(obj.entries, buffer, serializer) + } + override fun read(type: Class>, + buffer: BufferInput>, + serializer: Serializer): DistributedImmutableMap.Commands.PutAll { + return DistributedImmutableMap.Commands.PutAll(readMap(buffer, serializer)) + } } } register(LinkedHashMap::class.java) { @@ -170,4 +181,10 @@ private fun writeMap(map: Map<*, *>, buffer: BufferOutput>, } } -private fun readMap(buffer: BufferInput>, serializer: Serializer) = LinkedHashMap().apply { repeat(buffer.readInt()) { put(serializer.readObject(buffer), serializer.readObject(buffer)) } } +private fun readMap(buffer: BufferInput>, serializer: Serializer): LinkedHashMap { + return LinkedHashMap().apply { + repeat(buffer.readInt()) { + put(serializer.readObject(buffer), serializer.readObject(buffer)) + } + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt index ff0217d12d..deba64d1a3 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt @@ -1,17 +1,29 @@ package net.corda.node.services.transactions -import net.corda.core.flows.FlowLogic import net.corda.core.identity.Party +import net.corda.core.node.services.TrustedAuthorityNotaryService import net.corda.core.node.services.TimeWindowChecker +import net.corda.flows.NotaryFlow +import net.corda.node.services.api.ServiceHubInternal /** A validating notary service operated by a group of mutually trusting parties, uses the Raft algorithm to achieve consensus. */ -class RaftValidatingNotaryService(val timeWindowChecker: TimeWindowChecker, - val uniquenessProvider: RaftUniquenessProvider) : NotaryService { +class RaftValidatingNotaryService(override val services: ServiceHubInternal) : TrustedAuthorityNotaryService() { companion object { val type = ValidatingNotaryService.type.getSubType("raft") } - override val serviceFlowFactory: (Party, Int) -> FlowLogic = { otherParty, _ -> - ValidatingNotaryFlow(otherParty, timeWindowChecker, uniquenessProvider) + override val timeWindowChecker: TimeWindowChecker = TimeWindowChecker(services.clock) + override val uniquenessProvider: RaftUniquenessProvider = RaftUniquenessProvider(services) + + override fun createServiceFlow(otherParty: Party, platformVersion: Int): NotaryFlow.Service { + return ValidatingNotaryFlow(otherParty, this) + } + + override fun start() { + uniquenessProvider.start() + } + + override fun stop() { + uniquenessProvider.stop() } } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/SimpleNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/SimpleNotaryService.kt index 5ac707bc9e..c23d19532b 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/SimpleNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/SimpleNotaryService.kt @@ -1,19 +1,25 @@ package net.corda.node.services.transactions -import net.corda.core.flows.FlowLogic import net.corda.core.identity.Party +import net.corda.core.node.services.TrustedAuthorityNotaryService import net.corda.core.node.services.ServiceType import net.corda.core.node.services.TimeWindowChecker -import net.corda.core.node.services.UniquenessProvider +import net.corda.flows.NotaryFlow +import net.corda.node.services.api.ServiceHubInternal /** A simple Notary service that does not perform transaction validation */ -class SimpleNotaryService(val timeWindowChecker: TimeWindowChecker, - val uniquenessProvider: UniquenessProvider) : NotaryService { +class SimpleNotaryService(override val services: ServiceHubInternal) : TrustedAuthorityNotaryService() { companion object { val type = ServiceType.notary.getSubType("simple") } - override val serviceFlowFactory: (Party, Int) -> FlowLogic = { otherParty, _ -> - NonValidatingNotaryFlow(otherParty, timeWindowChecker, uniquenessProvider) + override val timeWindowChecker = TimeWindowChecker(services.clock) + override val uniquenessProvider = PersistentUniquenessProvider() + + override fun createServiceFlow(otherParty: Party, platformVersion: Int): NotaryFlow.Service { + return NonValidatingNotaryFlow(otherParty, this) } -} + + override fun start() {} + override fun stop() {} +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt index d30180db05..dca4e5f5ad 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt @@ -3,8 +3,7 @@ package net.corda.node.services.transactions import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.TransactionVerificationException import net.corda.core.identity.Party -import net.corda.core.node.services.TimeWindowChecker -import net.corda.core.node.services.UniquenessProvider +import net.corda.core.node.services.TrustedAuthorityNotaryService import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.unwrap @@ -17,10 +16,7 @@ import java.security.SignatureException * has its input states "blocked" by a transaction from another party, and needs to establish whether that transaction was * indeed valid. */ -class ValidatingNotaryFlow(otherSide: Party, - timeWindowChecker: TimeWindowChecker, - uniquenessProvider: UniquenessProvider) : - NotaryFlow.Service(otherSide, timeWindowChecker, uniquenessProvider) { +class ValidatingNotaryFlow(otherSide: Party, service: TrustedAuthorityNotaryService) : NotaryFlow.Service(otherSide, service) { /** * The received transaction is checked for contract-validity, which requires fully resolving it into a * [TransactionForVerification], for which the caller also has to to reveal the whole transaction diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt index 72b819e90a..c996a8979d 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt @@ -1,19 +1,25 @@ package net.corda.node.services.transactions -import net.corda.core.flows.FlowLogic import net.corda.core.identity.Party +import net.corda.core.node.services.TrustedAuthorityNotaryService import net.corda.core.node.services.ServiceType import net.corda.core.node.services.TimeWindowChecker -import net.corda.core.node.services.UniquenessProvider +import net.corda.flows.NotaryFlow +import net.corda.node.services.api.ServiceHubInternal /** A Notary service that validates the transaction chain of the submitted transaction before committing it */ -class ValidatingNotaryService(val timeWindowChecker: TimeWindowChecker, - val uniquenessProvider: UniquenessProvider) : NotaryService { +class ValidatingNotaryService(override val services: ServiceHubInternal) : TrustedAuthorityNotaryService() { companion object { val type = ServiceType.notary.getSubType("validating") } - override val serviceFlowFactory: (Party, Int) -> FlowLogic = { otherParty, _ -> - ValidatingNotaryFlow(otherParty, timeWindowChecker, uniquenessProvider) + override val timeWindowChecker = TimeWindowChecker(services.clock) + override val uniquenessProvider = PersistentUniquenessProvider() + + override fun createServiceFlow(otherParty: Party, platformVersion: Int): NotaryFlow.Service { + return ValidatingNotaryFlow(otherParty, this) } -} + + override fun start() {} + override fun stop() {} +} \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt b/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt index b7653f2a58..d82203d01a 100644 --- a/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt +++ b/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt @@ -10,6 +10,7 @@ import net.corda.core.transactions.SignedTransaction import net.corda.node.internal.InitiatedFlowFactory import net.corda.node.serialization.NodeClock import net.corda.node.services.api.* +import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.MessagingService import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.statemachine.FlowStateMachineImpl @@ -18,6 +19,7 @@ import net.corda.node.services.transactions.InMemoryTransactionVerifierService import net.corda.testing.MOCK_IDENTITY_SERVICE import net.corda.testing.node.MockNetworkMapCache import net.corda.testing.node.MockStorageService +import org.jetbrains.exposed.sql.Database import java.time.Clock open class MockServiceHubInternal( @@ -55,7 +57,10 @@ open class MockServiceHubInternal( get() = overrideClock ?: throw UnsupportedOperationException() override val myInfo: NodeInfo get() = throw UnsupportedOperationException() - + override val database: Database + get() = throw UnsupportedOperationException() + override val configuration: NodeConfiguration + get() = throw UnsupportedOperationException() override val monitoringService: MonitoringService = MonitoringService(MetricRegistry()) override val rpcFlows: List>> get() = throw UnsupportedOperationException() diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/InMemoryUniquenessProviderTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/InMemoryUniquenessProviderTests.kt deleted file mode 100644 index 8634d885be..0000000000 --- a/node/src/test/kotlin/net/corda/node/services/transactions/InMemoryUniquenessProviderTests.kt +++ /dev/null @@ -1,36 +0,0 @@ -package net.corda.node.services.transactions - -import net.corda.core.crypto.SecureHash -import net.corda.core.node.services.UniquenessException -import net.corda.testing.MEGA_CORP -import net.corda.testing.generateStateRef -import org.junit.Test -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith - -class InMemoryUniquenessProviderTests { - val identity = MEGA_CORP - val txID = SecureHash.randomSHA256() - - @Test fun `should commit a transaction with unused inputs without exception`() { - val provider = InMemoryUniquenessProvider() - val inputState = generateStateRef() - - provider.commit(listOf(inputState), txID, identity) - } - - @Test fun `should report a conflict for a transaction with previously used inputs`() { - val provider = InMemoryUniquenessProvider() - val inputState = generateStateRef() - - val inputs = listOf(inputState) - provider.commit(inputs, txID, identity) - - val ex = assertFailsWith { provider.commit(inputs, txID, identity) } - - val consumingTx = ex.error.stateHistory[inputState]!! - assertEquals(consumingTx.id, txID) - assertEquals(consumingTx.inputIndex, inputs.indexOf(inputState)) - assertEquals(consumingTx.requestingParty, identity) - } -} diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt index 44e762f1f8..2762a20236 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -30,7 +30,6 @@ import net.corda.node.services.messaging.MessagingService import net.corda.node.services.network.InMemoryNetworkMapService import net.corda.node.services.network.NetworkMapService import net.corda.node.services.transactions.InMemoryTransactionVerifierService -import net.corda.node.services.transactions.InMemoryUniquenessProvider import net.corda.node.services.transactions.SimpleNotaryService import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.node.services.vault.NodeVaultService @@ -225,8 +224,6 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, // There is no need to slow down the unit tests by initialising CityDatabase override fun findMyLocation(): WorldMapLocation? = null - override fun makeUniquenessProvider(type: ServiceType): UniquenessProvider = InMemoryUniquenessProvider() - override fun makeTransactionVerifierService() = InMemoryTransactionVerifierService(1) override fun myAddresses(): List = listOf(HostAndPort.fromHost("mockHost")) From a08f701dc5354f5012022d7daa7c5854f4ee38e9 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Thu, 29 Jun 2017 11:13:40 +0100 Subject: [PATCH 27/97] Removed the StorageService and puts its components directly into the service hub --- .../corda/client/jfx/NodeMonitorModelTest.kt | 2 +- .../client/jfx/model/NodeMonitorModel.kt | 2 +- .../net/corda/core/contracts/Structures.kt | 25 ++++--- .../net/corda/core/messaging/CordaRPCOps.kt | 4 +- .../kotlin/net/corda/core/node/ServiceHub.kt | 16 +++-- .../net/corda/core/node/services/Services.kt | 40 ----------- ...achineRecordedTransactionMappingStorage.kt | 19 ------ .../net/corda/core/serialization/Kryo.kt | 11 ++-- .../core/transactions/WireTransaction.kt | 2 +- .../net/corda/flows/FetchAttachmentsFlow.kt | 4 +- .../net/corda/flows/FetchTransactionsFlow.kt | 4 +- .../net/corda/flows/NotaryChangeFlow.kt | 4 +- .../corda/flows/ResolveTransactionsFlow.kt | 2 +- .../core/flows/ContractUpgradeFlowTest.kt | 16 ++--- .../core/flows/ResolveTransactionsFlowTest.kt | 12 ++-- .../core/node/AttachmentClassLoaderTests.kt | 8 +-- .../AttachmentSerializationTest.kt | 15 +++-- docs/source/api-service-hub.rst | 6 +- docs/source/changelog.rst | 7 +- .../corda/contracts/CommercialPaperTests.kt | 8 +-- .../net/corda/contracts/asset/CashTests.kt | 2 +- .../net/corda/node/internal/AbstractNode.kt | 66 ++++++++----------- .../corda/node/internal/CordaRPCOpsImpl.kt | 13 ++-- .../corda/node/services/CoreFlowHandlers.kt | 4 +- .../node/services/api/ServiceHubInternal.kt | 65 +++++++++++------- .../DBTransactionMappingStorage.kt | 5 +- .../persistence/DBTransactionStorage.kt | 5 +- ...achineRecordedTransactionMappingStorage.kt | 5 +- .../persistence/NodeAttachmentService.kt | 12 ++-- .../persistence/StorageServiceImpl.kt | 18 ----- .../statemachine/FlowStateMachineImpl.kt | 2 +- .../statemachine/StateMachineManager.kt | 6 +- .../services/vault/VaultQueryJavaTests.java | 4 +- .../corda/node/messaging/AttachmentTests.kt | 13 ++-- .../node/messaging/TwoPartyTradeFlowTests.kt | 49 +++++++------- .../node/services/MockServiceHubInternal.kt | 19 +++--- .../corda/node/services/NotaryChangeTests.kt | 2 +- .../database/HibernateConfigurationTest.kt | 2 +- .../services/vault/NodeVaultServiceTest.kt | 10 +-- .../node/services/vault/VaultQueryTests.kt | 6 +- .../node/services/vault/VaultWithCashTest.kt | 6 +- .../net/corda/traderdemo/flow/BuyerFlow.kt | 4 +- .../net/corda/traderdemo/flow/SellerFlow.kt | 2 +- .../main/kotlin/net/corda/testing/TestDSL.kt | 7 +- .../net/corda/testing/node/MockServices.kt | 22 ++----- 45 files changed, 240 insertions(+), 316 deletions(-) delete mode 100644 core/src/main/kotlin/net/corda/core/node/services/StateMachineRecordedTransactionMappingStorage.kt delete mode 100644 node/src/main/kotlin/net/corda/node/services/persistence/StorageServiceImpl.kt diff --git a/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt b/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt index 4d76aaa282..a3b9708610 100644 --- a/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt +++ b/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt @@ -12,12 +12,12 @@ import net.corda.core.flows.FlowInitiator import net.corda.core.flows.StateMachineRunId import net.corda.core.getOrThrow import net.corda.core.messaging.CordaRPCOps +import net.corda.core.messaging.StateMachineTransactionMapping import net.corda.core.messaging.StateMachineUpdate import net.corda.core.messaging.startFlow import net.corda.core.node.NodeInfo import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.ServiceInfo -import net.corda.core.node.services.StateMachineTransactionMapping import net.corda.core.node.services.Vault import net.corda.core.serialization.OpaqueBytes import net.corda.core.transactions.SignedTransaction diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt index 8505d1c621..5a5e79cc34 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt @@ -7,9 +7,9 @@ import net.corda.client.rpc.CordaRPCClientConfiguration import net.corda.core.flows.StateMachineRunId import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.StateMachineInfo +import net.corda.core.messaging.StateMachineTransactionMapping import net.corda.core.messaging.StateMachineUpdate import net.corda.core.node.services.NetworkMapCache.MapChange -import net.corda.core.node.services.StateMachineTransactionMapping import net.corda.core.node.services.Vault import net.corda.core.seconds import net.corda.core.transactions.SignedTransaction diff --git a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt index d10d9cc2bb..abfb9bad05 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt @@ -79,8 +79,7 @@ interface ContractState { * so that they receive the updated state, and don't end up in a situation where they can no longer use a state * they possess, since someone consumed that state during the notary change process. * - * The participants list should normally be derived from the contents of the state. E.g. for [Cash] the participants - * list should just contain the owner. + * The participants list should normally be derived from the contents of the state. */ val participants: List } @@ -126,7 +125,7 @@ infix fun T.withNotary(newNotary: Party) = TransactionState( * Definition for an issued product, which can be cash, a cash-like thing, assets, or generally anything else that's * quantifiable with integer quantities. * - * @param P the type of product underlying the definition, for example [Currency]. + * @param P the type of product underlying the definition, for example [java.util.Currency]. */ @CordaSerializable data class Issued(val issuer: PartyAndReference, val product: P) { @@ -159,8 +158,8 @@ interface Scheduled { } /** - * Represents a contract state (unconsumed output) of type [LinearState] and a point in time that a lifecycle event is expected to take place - * for that contract state. + * Represents a contract state (unconsumed output) of type [LinearState] and a point in time that a lifecycle event is + * expected to take place for that contract state. * * This is effectively the input to a scheduler, which wakes up at that point in time and asks the contract state what * lifecycle processing needs to take place. e.g. a fixing or a late payment etc. @@ -168,10 +167,11 @@ interface Scheduled { data class ScheduledStateRef(val ref: StateRef, override val scheduledAt: Instant) : Scheduled /** - * This class represents the lifecycle activity that a contract state of type [LinearState] would like to perform at a given point in time. - * e.g. run a fixing flow. + * This class represents the lifecycle activity that a contract state of type [LinearState] would like to perform at a + * given point in time. e.g. run a fixing flow. * - * Note the use of [FlowLogicRef] to represent a safe way to transport a [FlowLogic] out of the contract sandbox. + * Note the use of [FlowLogicRef] to represent a safe way to transport a [net.corda.core.flows.FlowLogic] out of the + * contract sandbox. * * Currently we support only flow based activities as we expect there to be a transaction generated off the back of * the activity, otherwise we have to start tracking secondary state on the platform of which scheduled activities @@ -383,9 +383,9 @@ class TimeWindow private constructor( // DOCSTART 5 /** * Implemented by a program that implements business logic on the shared ledger. All participants run this code for - * every [LedgerTransaction] they see on the network, for every input and output state. All contracts must accept the - * transaction for it to be accepted: failure of any aborts the entire thing. The time is taken from a trusted - * time-window attached to the transaction itself i.e. it is NOT necessarily the current time. + * every [net.corda.core.transactions.LedgerTransaction] they see on the network, for every input and output state. All + * contracts must accept the transaction for it to be accepted: failure of any aborts the entire thing. The time is taken + * from a trusted time-window attached to the transaction itself i.e. it is NOT necessarily the current time. * * TODO: Contract serialization is likely to change, so the annotation is likely temporary. */ @@ -461,9 +461,8 @@ interface Attachment : NamedByHash { abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment { companion object { fun SerializeAsTokenContext.attachmentDataLoader(id: SecureHash): () -> ByteArray { - val storage = serviceHub.storageService.attachments return { - val a = storage.openAttachment(id) ?: throw MissingAttachmentsException(listOf(id)) + val a = serviceHub.attachments.openAttachment(id) ?: throw MissingAttachmentsException(listOf(id)) (a as? AbstractAttachment)?.attachmentData ?: a.open().use { it.readBytes() } } } diff --git a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt index 7392201658..65cf172153 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt @@ -13,7 +13,6 @@ import net.corda.core.flows.StateMachineRunId import net.corda.core.identity.Party import net.corda.core.node.NodeInfo import net.corda.core.node.services.NetworkMapCache -import net.corda.core.node.services.StateMachineTransactionMapping import net.corda.core.node.services.Vault import net.corda.core.node.services.vault.PageSpecification import net.corda.core.node.services.vault.QueryCriteria @@ -48,6 +47,9 @@ sealed class StateMachineUpdate { data class Removed(override val id: StateMachineRunId, val result: ErrorOr<*>) : StateMachineUpdate() } +@CordaSerializable +data class StateMachineTransactionMapping(val stateMachineRunId: StateMachineRunId, val transactionId: SecureHash) + /** * RPC operations that the node exposes to clients using the Java client library. These can be called from * client apps and are implemented by the node in the [net.corda.node.internal.CordaRPCOpsImpl] class. diff --git a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt index 13038dc558..dc3ef5d458 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -17,8 +17,8 @@ import java.time.Clock */ interface ServicesForResolution { val identityService: IdentityService - val storageService: AttachmentsStorageService - + /** Provides access to storage of arbitrary JAR files (which may contain only data, no code). */ + val attachments: AttachmentStorage /** * Given a [StateRef] loads the referenced transaction and looks up the specified output [ContractState]. * @@ -40,7 +40,13 @@ interface ServiceHub : ServicesForResolution { val vaultService: VaultService val vaultQueryService: VaultQueryService val keyManagementService: KeyManagementService - override val storageService: StorageService + /** + * A map of hash->tx where tx has been signature/contract validated and the states are known to be correct. + * The signatures aren't technically needed after that point, but we keep them around so that we can relay + * the transaction data to other nodes that need it. + */ + val validatedTransactions: ReadOnlyTransactionStorage + val networkMapCache: NetworkMapCache val transactionVerifierService: TransactionVerifierService val clock: Clock @@ -77,7 +83,7 @@ interface ServiceHub : ServicesForResolution { */ @Throws(TransactionResolutionException::class) override fun loadState(stateRef: StateRef): TransactionState<*> { - val definingTx = storageService.validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash) + val definingTx = validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash) return definingTx.tx.outputs[stateRef.index] } @@ -87,7 +93,7 @@ interface ServiceHub : ServicesForResolution { * @throws IllegalProtocolLogicException or IllegalArgumentException if there are problems with the [logicType] or [args]. */ fun toStateAndRef(ref: StateRef): StateAndRef { - val definingTx = storageService.validatedTransactions.getTransaction(ref.txhash) ?: throw TransactionResolutionException(ref.txhash) + val definingTx = validatedTransactions.getTransaction(ref.txhash) ?: throw TransactionResolutionException(ref.txhash) return definingTx.tx.outRef(ref.index) } diff --git a/core/src/main/kotlin/net/corda/core/node/services/Services.kt b/core/src/main/kotlin/net/corda/core/node/services/Services.kt index 07198e8ab2..4ebfe100a4 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/Services.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/Services.kt @@ -22,12 +22,10 @@ import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction import net.corda.flows.AnonymisedIdentity -import org.bouncycastle.cert.X509CertificateHolder import rx.Observable import rx.subjects.PublishSubject import java.io.InputStream import java.security.PublicKey -import java.security.cert.CertPath import java.security.cert.X509Certificate import java.time.Instant import java.util.* @@ -528,44 +526,6 @@ interface FileUploader { fun accepts(type: String): Boolean } -interface AttachmentsStorageService { - /** Provides access to storage of arbitrary JAR files (which may contain only data, no code). */ - val attachments: AttachmentStorage - val attachmentsClassLoaderEnabled: Boolean -} - -/** - * A sketch of an interface to a simple key/value storage system. Intended for persistence of simple blobs like - * transactions, serialised flow state machines and so on. Again, this isn't intended to imply lack of SQL or - * anything like that, this interface is only big enough to support the prototyping work. - */ -interface StorageService : AttachmentsStorageService { - /** - * A map of hash->tx where tx has been signature/contract validated and the states are known to be correct. - * The signatures aren't technically needed after that point, but we keep them around so that we can relay - * the transaction data to other nodes that need it. - */ - val validatedTransactions: ReadOnlyTransactionStorage - - @Suppress("DEPRECATION") - @Deprecated("This service will be removed in a future milestone") - val uploaders: List - - val stateMachineRecordedTransactionMapping: StateMachineRecordedTransactionMappingStorage -} - -/** - * Storage service, with extensions to allow validated transactions to be added to. For use only within [ServiceHub]. - */ -interface TxWritableStorageService : StorageService { - /** - * A map of hash->tx where tx has been signature/contract validated and the states are known to be correct. - * The signatures aren't technically needed after that point, but we keep them around so that we can relay - * the transaction data to other nodes that need it. - */ - override val validatedTransactions: TransactionStorage -} - /** * Provides verification service. The implementation may be a simple in-memory verify() call or perhaps an IPC/RPC. */ diff --git a/core/src/main/kotlin/net/corda/core/node/services/StateMachineRecordedTransactionMappingStorage.kt b/core/src/main/kotlin/net/corda/core/node/services/StateMachineRecordedTransactionMappingStorage.kt deleted file mode 100644 index 98ca2272c2..0000000000 --- a/core/src/main/kotlin/net/corda/core/node/services/StateMachineRecordedTransactionMappingStorage.kt +++ /dev/null @@ -1,19 +0,0 @@ -package net.corda.core.node.services - -import net.corda.core.crypto.SecureHash -import net.corda.core.flows.StateMachineRunId -import net.corda.core.messaging.DataFeed -import net.corda.core.serialization.CordaSerializable -import rx.Observable - -@CordaSerializable -data class StateMachineTransactionMapping(val stateMachineRunId: StateMachineRunId, val transactionId: SecureHash) - -/** - * This is the interface to storage storing state machine -> recorded tx mappings. Any time a transaction is recorded - * during a flow run [addMapping] should be called. - */ -interface StateMachineRecordedTransactionMappingStorage { - fun addMapping(stateMachineRunId: StateMachineRunId, transactionId: SecureHash) - fun track(): DataFeed, StateMachineTransactionMapping> -} diff --git a/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt b/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt index 3289724162..4d3ab0fe5a 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt @@ -317,6 +317,9 @@ class MissingAttachmentsException(val ids: List) : Exception() /** A serialisation engine that knows how to deserialise code inside a sandbox */ @ThreadSafe object WireTransactionSerializer : Serializer() { + @VisibleForTesting + internal val attachmentsClassLoaderEnabled = "attachments.class.loader.enabled" + override fun write(kryo: Kryo, output: Output, obj: WireTransaction) { kryo.writeClassAndObject(output, obj.inputs) kryo.writeClassAndObject(output, obj.attachments) @@ -329,12 +332,12 @@ object WireTransactionSerializer : Serializer() { } private fun attachmentsClassLoader(kryo: Kryo, attachmentHashes: List): ClassLoader? { + kryo.context[attachmentsClassLoaderEnabled] as? Boolean ?: false || return null val serializationContext = kryo.serializationContext() ?: return null // Some tests don't set one. - serializationContext.serviceHub.storageService.attachmentsClassLoaderEnabled || return null val missing = ArrayList() val attachments = ArrayList() attachmentHashes.forEach { id -> - serializationContext.serviceHub.storageService.attachments.openAttachment(id)?.let { attachments += it } ?: run { missing += id } + serializationContext.serviceHub.attachments.openAttachment(id)?.let { attachments += it } ?: run { missing += id } } missing.isNotEmpty() && throw MissingAttachmentsException(missing) return AttachmentsClassLoader(attachments) @@ -635,7 +638,7 @@ object X500NameSerializer : Serializer() { */ @ThreadSafe object CertPathSerializer : Serializer() { - val factory = CertificateFactory.getInstance("X.509") + val factory: CertificateFactory = CertificateFactory.getInstance("X.509") override fun read(kryo: Kryo, input: Input, type: Class): CertPath { return factory.generateCertPath(input) } @@ -646,7 +649,7 @@ object CertPathSerializer : Serializer() { } /** - * For serialising an [CX509CertificateHolder] in an X.500 standard format. + * For serialising an [X509CertificateHolder] in an X.500 standard format. */ @ThreadSafe object X509CertificateSerializer : Serializer() { 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 ecc2c58be9..18ef251516 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt @@ -73,7 +73,7 @@ class WireTransaction( fun toLedgerTransaction(services: ServicesForResolution): LedgerTransaction { return toLedgerTransaction( resolveIdentity = { services.identityService.partyFromKey(it) }, - resolveAttachment = { services.storageService.attachments.openAttachment(it) }, + resolveAttachment = { services.attachments.openAttachment(it) }, resolveStateRef = { services.loadState(it) } ) } diff --git a/core/src/main/kotlin/net/corda/flows/FetchAttachmentsFlow.kt b/core/src/main/kotlin/net/corda/flows/FetchAttachmentsFlow.kt index 3ca8c8f695..805e25da14 100644 --- a/core/src/main/kotlin/net/corda/flows/FetchAttachmentsFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/FetchAttachmentsFlow.kt @@ -18,13 +18,13 @@ import net.corda.core.serialization.SerializeAsTokenContext class FetchAttachmentsFlow(requests: Set, otherSide: Party) : FetchDataFlow(requests, otherSide) { - override fun load(txid: SecureHash): Attachment? = serviceHub.storageService.attachments.openAttachment(txid) + override fun load(txid: SecureHash): Attachment? = serviceHub.attachments.openAttachment(txid) override fun convert(wire: ByteArray): Attachment = FetchedAttachment({ wire }) override fun maybeWriteToDisk(downloaded: List) { for (attachment in downloaded) { - serviceHub.storageService.attachments.importAttachment(attachment.open()) + serviceHub.attachments.importAttachment(attachment.open()) } } diff --git a/core/src/main/kotlin/net/corda/flows/FetchTransactionsFlow.kt b/core/src/main/kotlin/net/corda/flows/FetchTransactionsFlow.kt index 6e9c1055a8..0f99aad169 100644 --- a/core/src/main/kotlin/net/corda/flows/FetchTransactionsFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/FetchTransactionsFlow.kt @@ -17,7 +17,5 @@ import net.corda.core.transactions.SignedTransaction class FetchTransactionsFlow(requests: Set, otherSide: Party) : FetchDataFlow(requests, otherSide) { - override fun load(txid: SecureHash): SignedTransaction? { - return serviceHub.storageService.validatedTransactions.getTransaction(txid) - } + override fun load(txid: SecureHash): SignedTransaction? = serviceHub.validatedTransactions.getTransaction(txid) } diff --git a/core/src/main/kotlin/net/corda/flows/NotaryChangeFlow.kt b/core/src/main/kotlin/net/corda/flows/NotaryChangeFlow.kt index fff1fa3b3e..ee5453d167 100644 --- a/core/src/main/kotlin/net/corda/flows/NotaryChangeFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/NotaryChangeFlow.kt @@ -4,10 +4,8 @@ import net.corda.core.contracts.* import net.corda.core.flows.InitiatingFlow import net.corda.core.identity.AbstractParty 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 @@ -55,7 +53,7 @@ class NotaryChangeFlow( private fun resolveEncumbrances(tx: TransactionBuilder): Iterable { val stateRef = originalState.ref val txId = stateRef.txhash - val issuingTx = serviceHub.storageService.validatedTransactions.getTransaction(txId) + val issuingTx = serviceHub.validatedTransactions.getTransaction(txId) ?: throw StateReplacementException("Transaction $txId not found") val outputs = issuingTx.tx.outputs diff --git a/core/src/main/kotlin/net/corda/flows/ResolveTransactionsFlow.kt b/core/src/main/kotlin/net/corda/flows/ResolveTransactionsFlow.kt index 1b6d3d4b91..92f3b9ebd0 100644 --- a/core/src/main/kotlin/net/corda/flows/ResolveTransactionsFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/ResolveTransactionsFlow.kt @@ -193,7 +193,7 @@ class ResolveTransactionsFlow(private val txHashes: Set, private fun fetchMissingAttachments(downloads: List) { // TODO: This could be done in parallel with other fetches for extra speed. val missingAttachments = downloads.flatMap { wtx -> - wtx.attachments.filter { serviceHub.storageService.attachments.openAttachment(it) == null } + wtx.attachments.filter { serviceHub.attachments.openAttachment(it) == null } } if (missingAttachments.isNotEmpty()) subFlow(FetchAttachmentsFlow(missingAttachments.toSet(), otherSide)) diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt index 7160cf7c94..eb30dd855e 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -64,8 +64,8 @@ class ContractUpgradeFlowTest { a.services.startFlow(FinalityFlow(stx, setOf(a.info.legalIdentity, b.info.legalIdentity))) mockNet.runNetwork() - val atx = a.database.transaction { a.services.storageService.validatedTransactions.getTransaction(stx.id) } - val btx = b.database.transaction { b.services.storageService.validatedTransactions.getTransaction(stx.id) } + val atx = a.database.transaction { a.services.validatedTransactions.getTransaction(stx.id) } + val btx = b.database.transaction { b.services.validatedTransactions.getTransaction(stx.id) } requireNotNull(atx) requireNotNull(btx) @@ -85,13 +85,13 @@ class ContractUpgradeFlowTest { fun check(node: MockNetwork.MockNode) { val nodeStx = node.database.transaction { - node.services.storageService.validatedTransactions.getTransaction(result.ref.txhash) + node.services.validatedTransactions.getTransaction(result.ref.txhash) } requireNotNull(nodeStx) // Verify inputs. val input = node.database.transaction { - node.services.storageService.validatedTransactions.getTransaction(nodeStx!!.tx.inputs.single().txhash) + node.services.validatedTransactions.getTransaction(nodeStx!!.tx.inputs.single().txhash) } requireNotNull(input) assertTrue(input!!.tx.outputs.single().data is DummyContract.State) @@ -132,8 +132,8 @@ class ContractUpgradeFlowTest { mockNet.runNetwork() handle.returnValue.getOrThrow() - val atx = a.database.transaction { a.services.storageService.validatedTransactions.getTransaction(stx.id) } - val btx = b.database.transaction { b.services.storageService.validatedTransactions.getTransaction(stx.id) } + val atx = a.database.transaction { a.services.validatedTransactions.getTransaction(stx.id) } + val btx = b.database.transaction { b.services.validatedTransactions.getTransaction(stx.id) } requireNotNull(atx) requireNotNull(btx) @@ -156,11 +156,11 @@ class ContractUpgradeFlowTest { val result = resultFuture.getOrThrow() // Check results. listOf(a, b).forEach { - val signedTX = a.database.transaction { a.services.storageService.validatedTransactions.getTransaction(result.ref.txhash) } + val signedTX = a.database.transaction { a.services.validatedTransactions.getTransaction(result.ref.txhash) } requireNotNull(signedTX) // Verify inputs. - val input = a.database.transaction { a.services.storageService.validatedTransactions.getTransaction(signedTX!!.tx.inputs.single().txhash) } + val input = a.database.transaction { a.services.validatedTransactions.getTransaction(signedTX!!.tx.inputs.single().txhash) } requireNotNull(input) assertTrue(input!!.tx.outputs.single().data is DummyContract.State) diff --git a/core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt index 887e1649dc..c80ed66f30 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt @@ -58,8 +58,8 @@ class ResolveTransactionsFlowTest { val results = future.getOrThrow() assertEquals(listOf(stx1.id, stx2.id), results.map { it.id }) b.database.transaction { - assertEquals(stx1, b.storage.validatedTransactions.getTransaction(stx1.id)) - assertEquals(stx2, b.storage.validatedTransactions.getTransaction(stx2.id)) + assertEquals(stx1, b.services.validatedTransactions.getTransaction(stx1.id)) + assertEquals(stx2, b.services.validatedTransactions.getTransaction(stx2.id)) } } // DOCEND 1 @@ -81,9 +81,9 @@ class ResolveTransactionsFlowTest { mockNet.runNetwork() future.getOrThrow() b.database.transaction { - assertEquals(stx1, b.storage.validatedTransactions.getTransaction(stx1.id)) + assertEquals(stx1, b.services.validatedTransactions.getTransaction(stx1.id)) // But stx2 wasn't inserted, just stx1. - assertNull(b.storage.validatedTransactions.getTransaction(stx2.id)) + assertNull(b.services.validatedTransactions.getTransaction(stx2.id)) } } @@ -148,7 +148,7 @@ class ResolveTransactionsFlowTest { } // TODO: this operation should not require an explicit transaction val id = a.database.transaction { - a.services.storageService.attachments.importAttachment(makeJar()) + a.services.attachments.importAttachment(makeJar()) } val stx2 = makeTransactions(withAttachment = id).second val p = ResolveTransactionsFlow(stx2, a.info.legalIdentity) @@ -158,7 +158,7 @@ class ResolveTransactionsFlowTest { // TODO: this operation should not require an explicit transaction b.database.transaction { - assertNotNull(b.services.storageService.attachments.openAttachment(id)) + assertNotNull(b.services.attachments.openAttachment(id)) } } diff --git a/core/src/test/kotlin/net/corda/core/node/AttachmentClassLoaderTests.kt b/core/src/test/kotlin/net/corda/core/node/AttachmentClassLoaderTests.kt index 127103ae5f..5a58def9fd 100644 --- a/core/src/test/kotlin/net/corda/core/node/AttachmentClassLoaderTests.kt +++ b/core/src/test/kotlin/net/corda/core/node/AttachmentClassLoaderTests.kt @@ -8,7 +8,6 @@ import net.corda.core.crypto.SecureHash import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.node.services.AttachmentStorage -import net.corda.core.node.services.StorageService import net.corda.core.serialization.* import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.DUMMY_NOTARY @@ -22,7 +21,6 @@ import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.net.URL import java.net.URLClassLoader -import java.security.PublicKey import java.util.jar.JarOutputStream import java.util.zip.ZipEntry import kotlin.test.assertEquals @@ -42,11 +40,9 @@ class AttachmentClassLoaderTests { val ISOLATED_CONTRACTS_JAR_PATH: URL = AttachmentClassLoaderTests::class.java.getResource("isolated.jar") private fun Kryo.withAttachmentStorage(attachmentStorage: AttachmentStorage, block: () -> T) = run { + context.put(WireTransactionSerializer.attachmentsClassLoaderEnabled, true) val serviceHub = mock() - val storageService = mock() - whenever(serviceHub.storageService).thenReturn(storageService) - whenever(storageService.attachmentsClassLoaderEnabled).thenReturn(true) - whenever(storageService.attachments).thenReturn(attachmentStorage) + whenever(serviceHub.attachments).thenReturn(attachmentStorage) withSerializationContext(SerializeAsTokenContext(serviceHub) {}, block) } } diff --git a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt index 1198508986..f1259a4f73 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt @@ -42,9 +42,12 @@ private fun createAttachmentData(content: String) = ByteArrayOutputStream().appl private fun Attachment.extractContent() = ByteArrayOutputStream().apply { extractFile("content", this) }.toString(UTF_8.name()) -private fun MockNetwork.MockNode.attachments() = services.storageService.attachments as NodeAttachmentService -private fun MockNetwork.MockNode.saveAttachment(content: String) = database.transaction { attachments().importAttachment(createAttachmentData(content).inputStream()) } -private fun MockNetwork.MockNode.hackAttachment(attachmentId: SecureHash, content: String) = database.transaction { attachments().updateAttachment(attachmentId, createAttachmentData(content)) } +private fun MockNetwork.MockNode.saveAttachment(content: String) = database.transaction { + attachments.importAttachment(createAttachmentData(content).inputStream()) +} +private fun MockNetwork.MockNode.hackAttachment(attachmentId: SecureHash, content: String) = database.transaction { + attachments.updateAttachment(attachmentId, createAttachmentData(content)) +} /** * @see NodeAttachmentService.importAttachment @@ -122,7 +125,7 @@ class AttachmentSerializationTest { private class OpenAttachmentLogic(server: MockNetwork.MockNode, private val attachmentId: SecureHash) : ClientLogic(server) { @Suspendable override fun getAttachmentContent(): String { - val localAttachment = serviceHub.storageService.attachments.openAttachment(attachmentId)!! + val localAttachment = serviceHub.attachments.openAttachment(attachmentId)!! communicate() return localAttachment.extractContent() } @@ -153,7 +156,7 @@ class AttachmentSerializationTest { override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, advertisedServices: Set, id: Int, overrideServices: Map?, entropyRoot: BigInteger): MockNetwork.MockNode { return object : MockNetwork.MockNode(config, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) { override fun startMessagingService(rpcOps: RPCOps) { - attachments().checkAttachmentsOnLoad = checkAttachmentsOnLoad + attachments.checkAttachmentsOnLoad = checkAttachmentsOnLoad super.startMessagingService(rpcOps) } } @@ -180,7 +183,7 @@ class AttachmentSerializationTest { @Test fun `only the hash of a regular attachment should be saved in checkpoint`() { val attachmentId = client.saveAttachment("genuine") - client.attachments().checkAttachmentsOnLoad = false // Cached by AttachmentImpl. + client.attachments.checkAttachmentsOnLoad = false // Cached by AttachmentImpl. launchFlow(OpenAttachmentLogic(server, attachmentId), 1) client.hackAttachment(attachmentId, "hacked") assertEquals("hacked", rebootClientAndGetAttachmentContent(false)) // Pass in false to allow non-genuine data to be loaded. diff --git a/docs/source/api-service-hub.rst b/docs/source/api-service-hub.rst index 739dee94bd..d3cea14024 100644 --- a/docs/source/api-service-hub.rst +++ b/docs/source/api-service-hub.rst @@ -7,10 +7,12 @@ various services the node provides. The services offered by the ``ServiceHub`` a * Provides information on other nodes on the network (e.g. notaries…) * ``ServiceHub.identityService`` * Allows you to resolve anonymous identities to well-known identities if you have the required certificates +* ``ServiceHub.attachments`` + * Gives you access to the node's attachments +* ``ServiceHub.validatedTransactions`` + * Gives you access to the transactions stored in the node * ``ServiceHub.vaultService`` * Stores the node’s current and historic states -* ``ServiceHub.storageService`` - * Stores additional information such as transactions and attachments * ``ServiceHub.keyManagementService`` * Manages signing transactions and generating fresh public keys * ``ServiceHub.myInfo`` diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index ee30fee6c3..3c9d316545 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -6,6 +6,7 @@ from the previous milestone release. UNRELEASED ---------- + * Changes in ``NodeInfo``: * ``PhysicalLocation`` was renamed to ``WorldMapLocation`` to emphasise that it doesn't need to map to a truly physical @@ -13,8 +14,12 @@ UNRELEASED * Slots for multiple IP addresses and ``legalIdentitiesAndCert``s were introduced. Addresses are no longer of type ``SingleMessageRecipient``, but of ``HostAndPort``. +* ``ServiceHub.storageService`` has been removed. ``attachments`` and ``validatedTransactions`` are now direct members of + ``ServiceHub``. + Milestone 13 ----------- +------------ + Special thank you to `Frederic Dalibard `_, for his contribution which adds support for more currencies to the DemoBench and Explorer tools. diff --git a/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt b/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt index 4b9c409a85..7dfe0e83c3 100644 --- a/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt @@ -220,7 +220,7 @@ class CommercialPaperTestsGeneric { override fun recordTransactions(txs: Iterable) { for (stx in txs) { - storageService.validatedTransactions.addTransaction(stx) + validatedTransactions.addTransaction(stx) } // Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions. vaultService.notifyAll(txs.map { it.tx }) @@ -240,7 +240,7 @@ class CommercialPaperTestsGeneric { override fun recordTransactions(txs: Iterable) { for (stx in txs) { - storageService.validatedTransactions.addTransaction(stx) + validatedTransactions.addTransaction(stx) } // Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions. vaultService.notifyAll(txs.map { it.tx }) @@ -251,8 +251,8 @@ class CommercialPaperTestsGeneric { } // Propagate the cash transactions to each side. - aliceServices.recordTransactions(bigCorpVault.states.map { bigCorpServices.storageService.validatedTransactions.getTransaction(it.ref.txhash)!! }) - bigCorpServices.recordTransactions(alicesVault.states.map { aliceServices.storageService.validatedTransactions.getTransaction(it.ref.txhash)!! }) + aliceServices.recordTransactions(bigCorpVault.states.map { bigCorpServices.validatedTransactions.getTransaction(it.ref.txhash)!! }) + bigCorpServices.recordTransactions(alicesVault.states.map { aliceServices.validatedTransactions.getTransaction(it.ref.txhash)!! }) // BigCorp™ issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself. val faceValue = 10000.DOLLARS `issued by` DUMMY_CASH_ISSUER diff --git a/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt b/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt index 637d5d62a5..2e822f6a7a 100644 --- a/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt @@ -63,7 +63,7 @@ class CashTests { override fun recordTransactions(txs: Iterable) { for (stx in txs) { - storageService.validatedTransactions.addTransaction(stx) + validatedTransactions.addTransaction(stx) } // Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions. vaultService.notifyAll(txs.map { it.tx }) diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 18fef52f56..65d1c7052e 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -45,7 +45,10 @@ import net.corda.node.services.network.NetworkMapService import net.corda.node.services.network.NetworkMapService.RegistrationResponse import net.corda.node.services.network.NodeRegistration import net.corda.node.services.network.PersistentNetworkMapService -import net.corda.node.services.persistence.* +import net.corda.node.services.persistence.DBCheckpointStorage +import net.corda.node.services.persistence.DBTransactionMappingStorage +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 @@ -70,7 +73,6 @@ import java.lang.reflect.InvocationTargetException import java.lang.reflect.Modifier.* import java.net.JarURLConnection import java.net.URI -import java.nio.file.FileAlreadyExistsException import java.nio.file.Path import java.nio.file.Paths import java.security.KeyPair @@ -120,10 +122,14 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, private val flowFactories = ConcurrentHashMap>, InitiatedFlowFactory<*>>() protected val partyKeys = mutableSetOf() - val services = object : ServiceHubInternal() { + val services = object : ServiceHubInternal { + override val attachments: AttachmentStorage get() = this@AbstractNode.attachments + override val uploaders: List get() = this@AbstractNode.uploaders + override val stateMachineRecordedTransactionMapping: StateMachineRecordedTransactionMappingStorage + get() = this@AbstractNode.transactionMappings + override val validatedTransactions: TransactionStorage get() = this@AbstractNode.transactions override val networkService: MessagingService get() = network override val networkMapCache: NetworkMapCacheInternal get() = netMapCache - override val storageService: TxWritableStorageService get() = storage override val vaultService: VaultService get() = vault override val vaultQueryService: VaultQueryService get() = vaultQuery override val keyManagementService: KeyManagementService get() = keyManagement @@ -157,7 +163,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, override fun recordTransactions(txs: Iterable) { database.transaction { - recordTransactionsInternal(storage, txs) + super.recordTransactions(txs) } } } @@ -167,9 +173,12 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, } lateinit var info: NodeInfo - lateinit var storage: TxWritableStorageService lateinit var checkpointStorage: CheckpointStorage lateinit var smm: StateMachineManager + lateinit var attachments: NodeAttachmentService + lateinit var transactions: TransactionStorage + lateinit var transactionMappings: StateMachineRecordedTransactionMappingStorage + lateinit var uploaders: List lateinit var vault: VaultService lateinit var vaultQuery: VaultQueryService lateinit var keyManagement: KeyManagementService @@ -469,9 +478,10 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, */ private fun makeServices(keyStoreWrapper: KeyStoreWrapper): MutableList { val keyStore = keyStoreWrapper.keyStore - val storageServices = initialiseStorageService(configuration.baseDirectory) - storage = storageServices.first - checkpointStorage = storageServices.second + attachments = createAttachmentStorage() + transactions = createTransactionStorage() + transactionMappings = DBTransactionMappingStorage() + checkpointStorage = DBCheckpointStorage() netMapCache = InMemoryNetworkMapCache(services) network = makeMessagingService() schemas = makeSchemaService() @@ -490,11 +500,13 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, keyManagement = makeKeyManagementService(identity) scheduler = NodeSchedulerService(services, database, unfinishedSchedules = busyNodeLatch) - val tokenizableServices = mutableListOf(storage, network, vault, vaultQuery, keyManagement, identity, platformClock, scheduler) + val tokenizableServices = mutableListOf(attachments, network, vault, vaultQuery, keyManagement, identity, platformClock, scheduler) makeAdvertisedServices(tokenizableServices) return tokenizableServices } + protected open fun createTransactionStorage(): TransactionStorage = DBTransactionStorage() + private fun scanCordapps(): ScanResult? { val scanPackage = System.getProperty("net.corda.node.cordapp.scan.package") val paths = if (scanPackage != null) { @@ -548,9 +560,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, } private fun initUploaders() { - val uploaders: List = listOf(storage.attachments as NodeAttachmentService) + - cordappServices.values.filterIsInstance(AcceptsFileUpload::class.java) - (storage as StorageServiceImpl).initUploaders(uploaders) + uploaders = listOf(attachments) + cordappServices.values.filterIsInstance(AcceptsFileUpload::class.java) } private fun makeVaultObservers() { @@ -625,7 +635,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, * Run any tasks that are needed to ensure the node is in a correct state before running start(). */ open fun setup(): AbstractNode { - createNodeDir() + configuration.baseDirectory.createDirectories() return this } @@ -761,22 +771,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, protected abstract fun startMessagingService(rpcOps: RPCOps) - protected open fun initialiseStorageService(dir: Path): Pair { - val attachments = makeAttachmentStorage(dir) - val checkpointStorage = DBCheckpointStorage() - val transactionStorage = DBTransactionStorage() - val stateMachineTransactionMappingStorage = DBTransactionMappingStorage() - return Pair( - constructStorageService(attachments, transactionStorage, stateMachineTransactionMappingStorage), - checkpointStorage - ) - } - - protected open fun constructStorageService(attachments: AttachmentStorage, - transactionStorage: TransactionStorage, - stateMachineRecordedTransactionMappingStorage: StateMachineRecordedTransactionMappingStorage) = - StorageServiceImpl(attachments, transactionStorage, stateMachineRecordedTransactionMappingStorage) - protected fun obtainLegalIdentity(): PartyAndCertificate = identityKeyPair.first protected fun obtainLegalIdentityKey(): KeyPair = identityKeyPair.second private val identityKeyPair by lazy { obtainKeyPair("identity", configuration.myLegalName) } @@ -846,18 +840,10 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, protected open fun generateKeyPair() = cryptoGenerateKeyPair() - protected fun makeAttachmentStorage(dir: Path): AttachmentStorage { - val attachmentsDir = dir / "attachments" - try { - attachmentsDir.createDirectory() - } catch (e: FileAlreadyExistsException) { - } + private fun createAttachmentStorage(): NodeAttachmentService { + val attachmentsDir = (configuration.baseDirectory / "attachments").createDirectories() return NodeAttachmentService(attachmentsDir, configuration.dataSourceProperties, services.monitoringService.metrics) } - - protected fun createNodeDir() { - configuration.baseDirectory.createDirectories() - } } private class KeyStoreWrapper(val keyStore: KeyStore, val storePath: Path, private val storePassword: String) { diff --git a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt index f8d4f77378..cec3445f1a 100644 --- a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt @@ -12,7 +12,6 @@ import net.corda.core.identity.Party import net.corda.core.messaging.* import net.corda.core.node.NodeInfo import net.corda.core.node.services.NetworkMapCache -import net.corda.core.node.services.StateMachineTransactionMapping import net.corda.core.node.services.Vault import net.corda.core.node.services.vault.PageSpecification import net.corda.core.node.services.vault.QueryCriteria @@ -76,7 +75,7 @@ class CordaRPCOpsImpl( override fun verifiedTransactionsFeed(): DataFeed, SignedTransaction> { return database.transaction { - services.storageService.validatedTransactions.track() + services.validatedTransactions.track() } } @@ -92,7 +91,7 @@ class CordaRPCOpsImpl( override fun stateMachineRecordedTransactionMappingFeed(): DataFeed, StateMachineTransactionMapping> { return database.transaction { - services.storageService.stateMachineRecordedTransactionMapping.track() + services.stateMachineRecordedTransactionMapping.track() } } @@ -143,21 +142,21 @@ class CordaRPCOpsImpl( override fun attachmentExists(id: SecureHash): Boolean { // TODO: this operation should not require an explicit transaction return database.transaction { - services.storageService.attachments.openAttachment(id) != null + services.attachments.openAttachment(id) != null } } override fun openAttachment(id: SecureHash): InputStream { // TODO: this operation should not require an explicit transaction return database.transaction { - services.storageService.attachments.openAttachment(id)!!.open() + services.attachments.openAttachment(id)!!.open() } } override fun uploadAttachment(jar: InputStream): SecureHash { // TODO: this operation should not require an explicit transaction return database.transaction { - services.storageService.attachments.importAttachment(jar) + services.attachments.importAttachment(jar) } } @@ -166,7 +165,7 @@ class CordaRPCOpsImpl( override fun currentNodeTime(): Instant = Instant.now(services.clock) @Suppress("OverridingDeprecatedMember", "DEPRECATION") override fun uploadFile(dataType: String, name: String?, file: InputStream): String { - val acceptor = services.storageService.uploaders.firstOrNull { it.accepts(dataType) } + val acceptor = services.uploaders.firstOrNull { it.accepts(dataType) } return database.transaction { acceptor?.upload(file) ?: throw RuntimeException("Cannot find file upload acceptor for $dataType") } diff --git a/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt b/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt index e6b8c0e6e9..670834ff08 100644 --- a/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt +++ b/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt @@ -27,14 +27,14 @@ import net.corda.flows.* */ class FetchTransactionsHandler(otherParty: Party) : FetchDataHandler(otherParty) { override fun getData(id: SecureHash): SignedTransaction? { - return serviceHub.storageService.validatedTransactions.getTransaction(id) + return serviceHub.validatedTransactions.getTransaction(id) } } // TODO: Use Artemis message streaming support here, called "large messages". This avoids the need to buffer. class FetchAttachmentsHandler(otherParty: Party) : FetchDataHandler(otherParty) { override fun getData(id: SecureHash): ByteArray? { - return serviceHub.storageService.attachments.openAttachment(id)?.open()?.readBytes() + return serviceHub.attachments.openAttachment(id)?.open()?.readBytes() } } diff --git a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt index 65c48f39b6..ef427d4b65 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt @@ -1,16 +1,20 @@ package net.corda.node.services.api import com.google.common.annotations.VisibleForTesting -import com.google.common.net.HostAndPort import com.google.common.util.concurrent.ListenableFuture +import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowInitiator import net.corda.core.flows.FlowLogic +import net.corda.core.flows.StateMachineRunId import net.corda.core.internal.FlowStateMachine +import net.corda.core.messaging.DataFeed import net.corda.core.messaging.SingleMessageRecipient +import net.corda.core.messaging.StateMachineTransactionMapping import net.corda.core.node.NodeInfo import net.corda.core.node.PluginServiceHub +import net.corda.core.node.services.FileUploader import net.corda.core.node.services.NetworkMapCache -import net.corda.core.node.services.TxWritableStorageService +import net.corda.core.node.services.TransactionStorage import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.loggerFor @@ -58,34 +62,38 @@ sealed class NetworkCacheError : Exception() { class DeregistrationFailed : NetworkCacheError() } -abstract class ServiceHubInternal : PluginServiceHub { +interface ServiceHubInternal : PluginServiceHub { companion object { private val log = loggerFor() } - abstract val monitoringService: MonitoringService - abstract val schemaService: SchemaService - abstract override val networkMapCache: NetworkMapCacheInternal - abstract val schedulerService: SchedulerService - abstract val auditService: AuditService - abstract val rpcFlows: List>> - abstract val networkService: MessagingService - abstract val database: Database - abstract val configuration: NodeConfiguration - /** - * Given a list of [SignedTransaction]s, writes them to the given storage for validated transactions and then - * sends them to the vault for further processing. This is intended for implementations to call from - * [recordTransactions]. - * - * @param txs The transactions to record. + * A map of hash->tx where tx has been signature/contract validated and the states are known to be correct. + * The signatures aren't technically needed after that point, but we keep them around so that we can relay + * the transaction data to other nodes that need it. */ - internal fun recordTransactionsInternal(writableStorageService: TxWritableStorageService, txs: Iterable) { + override val validatedTransactions: TransactionStorage + val stateMachineRecordedTransactionMapping: StateMachineRecordedTransactionMappingStorage + val monitoringService: MonitoringService + val schemaService: SchemaService + override val networkMapCache: NetworkMapCacheInternal + val schedulerService: SchedulerService + val auditService: AuditService + val rpcFlows: List>> + val networkService: MessagingService + val database: Database + val configuration: NodeConfiguration + + @Suppress("DEPRECATION") + @Deprecated("This service will be removed in a future milestone") + val uploaders: List + + override fun recordTransactions(txs: Iterable) { val stateMachineRunId = FlowStateMachineImpl.currentStateMachine()?.id - val recordedTransactions = txs.filter { writableStorageService.validatedTransactions.addTransaction(it) } + val recordedTransactions = txs.filter { validatedTransactions.addTransaction(it) } if (stateMachineRunId != null) { recordedTransactions.forEach { - storageService.stateMachineRecordedTransactionMapping.addMapping(stateMachineRunId, it.id) + stateMachineRecordedTransactionMapping.addMapping(stateMachineRunId, it.id) } } else { log.warn("Transactions recorded from outside of a state machine") @@ -104,7 +112,7 @@ abstract class ServiceHubInternal : PluginServiceHub { * Starts an already constructed flow. Note that you must be on the server thread to call this method. * @param flowInitiator indicates who started the flow, see: [FlowInitiator]. */ - abstract fun startFlow(logic: FlowLogic, flowInitiator: FlowInitiator): FlowStateMachineImpl + fun startFlow(logic: FlowLogic, flowInitiator: FlowInitiator): FlowStateMachineImpl /** * Will check [logicType] and [args] against a whitelist and if acceptable then construct and initiate the flow. @@ -124,5 +132,14 @@ abstract class ServiceHubInternal : PluginServiceHub { return startFlow(logic, flowInitiator) } - abstract fun getFlowFactory(initiatingFlowClass: Class>): InitiatedFlowFactory<*>? -} \ No newline at end of file + fun getFlowFactory(initiatingFlowClass: Class>): InitiatedFlowFactory<*>? +} + +/** + * This is the interface to storage storing state machine -> recorded tx mappings. Any time a transaction is recorded + * during a flow run [addMapping] should be called. + */ +interface StateMachineRecordedTransactionMappingStorage { + fun addMapping(stateMachineRunId: StateMachineRunId, transactionId: SecureHash) + fun track(): DataFeed, StateMachineTransactionMapping> +} diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionMappingStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionMappingStorage.kt index bbf58c3524..b9c376b768 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionMappingStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionMappingStorage.kt @@ -5,12 +5,11 @@ import net.corda.core.bufferUntilSubscribed import net.corda.core.crypto.SecureHash import net.corda.core.flows.StateMachineRunId import net.corda.core.messaging.DataFeed -import net.corda.core.node.services.StateMachineRecordedTransactionMappingStorage -import net.corda.core.node.services.StateMachineTransactionMapping +import net.corda.core.messaging.StateMachineTransactionMapping +import net.corda.node.services.api.StateMachineRecordedTransactionMappingStorage import net.corda.node.utilities.* import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.statements.InsertStatement -import rx.Observable import rx.subjects.PublishSubject import javax.annotation.concurrent.ThreadSafe diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt index 51139177f1..149d6db12f 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt @@ -5,6 +5,7 @@ import net.corda.core.bufferUntilSubscribed import net.corda.core.crypto.SecureHash import net.corda.core.messaging.DataFeed import net.corda.core.node.services.TransactionStorage +import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.SignedTransaction import net.corda.node.utilities.* import org.jetbrains.exposed.sql.ResultRow @@ -14,7 +15,7 @@ import rx.Observable import rx.subjects.PublishSubject import java.util.Collections.synchronizedMap -class DBTransactionStorage : TransactionStorage { +class DBTransactionStorage : TransactionStorage, SingletonSerializeAsToken() { private object Table : JDBCHashedTable("${NODE_DATABASE_PREFIX}transactions") { val txId = secureHash("tx_id") val transaction = blob("transaction") @@ -59,7 +60,7 @@ class DBTransactionStorage : TransactionStorage { } } - val updatesPublisher = PublishSubject.create().toSerialized() + private val updatesPublisher = PublishSubject.create().toSerialized() override val updates: Observable = updatesPublisher.wrapWithDatabaseTransaction() override fun track(): DataFeed, SignedTransaction> { diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/InMemoryStateMachineRecordedTransactionMappingStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/InMemoryStateMachineRecordedTransactionMappingStorage.kt index f0aaa50e86..168fee3bc8 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/InMemoryStateMachineRecordedTransactionMappingStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/InMemoryStateMachineRecordedTransactionMappingStorage.kt @@ -5,9 +5,8 @@ import net.corda.core.bufferUntilSubscribed import net.corda.core.crypto.SecureHash import net.corda.core.flows.StateMachineRunId import net.corda.core.messaging.DataFeed -import net.corda.core.node.services.StateMachineRecordedTransactionMappingStorage -import net.corda.core.node.services.StateMachineTransactionMapping -import rx.Observable +import net.corda.core.messaging.StateMachineTransactionMapping +import net.corda.node.services.api.StateMachineRecordedTransactionMappingStorage import rx.subjects.PublishSubject import java.util.* import javax.annotation.concurrent.ThreadSafe diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt index 0fecc0049f..06f60228c1 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt @@ -14,10 +14,7 @@ import net.corda.core.div import net.corda.core.extractZipFile import net.corda.core.isDirectory import net.corda.core.node.services.AttachmentStorage -import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.SerializationToken -import net.corda.core.serialization.SerializeAsToken -import net.corda.core.serialization.SerializeAsTokenContext +import net.corda.core.serialization.* import net.corda.core.utilities.loggerFor import net.corda.node.services.api.AcceptsFileUpload import net.corda.node.services.database.RequeryConfiguration @@ -38,8 +35,11 @@ import javax.annotation.concurrent.ThreadSafe * Stores attachments in H2 database. */ @ThreadSafe -class NodeAttachmentService(override var storePath: Path, dataSourceProperties: Properties, metrics: MetricRegistry) : AttachmentStorage, AcceptsFileUpload { - private val log = loggerFor() +class NodeAttachmentService(override var storePath: Path, dataSourceProperties: Properties, metrics: MetricRegistry) + : AttachmentStorage, AcceptsFileUpload, SingletonSerializeAsToken() { + companion object { + private val log = loggerFor() + } val configuration = RequeryConfiguration(dataSourceProperties) val session = configuration.sessionForModel(Models.PERSISTENCE) diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/StorageServiceImpl.kt b/node/src/main/kotlin/net/corda/node/services/persistence/StorageServiceImpl.kt deleted file mode 100644 index c90cf27997..0000000000 --- a/node/src/main/kotlin/net/corda/node/services/persistence/StorageServiceImpl.kt +++ /dev/null @@ -1,18 +0,0 @@ -package net.corda.node.services.persistence - -import net.corda.core.node.services.* -import net.corda.core.serialization.SingletonSerializeAsToken - -open class StorageServiceImpl(override val attachments: AttachmentStorage, - override val validatedTransactions: TransactionStorage, - override val stateMachineRecordedTransactionMapping: StateMachineRecordedTransactionMappingStorage) - : SingletonSerializeAsToken(), TxWritableStorageService { - override val attachmentsClassLoaderEnabled = false - - lateinit override var uploaders: List - - fun initUploaders(uploadersList: List) { - @Suppress("DEPRECATION") - uploaders = uploadersList - } -} diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt index 0716b52beb..0b7d085b18 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt @@ -205,7 +205,7 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, override fun waitForLedgerCommit(hash: SecureHash, sessionFlow: FlowLogic<*>): SignedTransaction { logger.debug { "waitForLedgerCommit($hash) ..." } suspend(WaitForLedgerCommit(hash, sessionFlow.stateMachine as FlowStateMachineImpl<*>)) - val stx = serviceHub.storageService.validatedTransactions.getTransaction(hash) + val stx = serviceHub.validatedTransactions.getTransaction(hash) if (stx != null) { logger.debug { "Transaction $hash committed to ledger" } return stx diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt index eac032e0e3..1e274ebf9a 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt @@ -187,7 +187,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, private fun listenToLedgerTransactions() { // Observe the stream of committed, validated transactions and resume fibers that are waiting for them. - serviceHub.storageService.validatedTransactions.updates.subscribe { stx -> + serviceHub.validatedTransactions.updates.subscribe { stx -> val hash = stx.id val fibers: Set> = mutex.locked { fibersWaitingForLedgerCommit.removeAll(hash) } if (fibers.isNotEmpty()) { @@ -268,7 +268,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, if (waitingForResponse != null) { if (waitingForResponse is WaitForLedgerCommit) { val stx = database.transaction { - serviceHub.storageService.validatedTransactions.getTransaction(waitingForResponse.hash) + serviceHub.validatedTransactions.getTransaction(waitingForResponse.hash) } if (stx != null) { fiber.logger.info("Resuming fiber as tx ${waitingForResponse.hash} has committed") @@ -548,7 +548,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, private fun processWaitForCommitRequest(ioRequest: WaitForLedgerCommit) { // Is it already committed? val stx = database.transaction { - serviceHub.storageService.validatedTransactions.getTransaction(ioRequest.hash) + serviceHub.validatedTransactions.getTransaction(ioRequest.hash) } if (stx != null) { resumeFiber(ioRequest.fiber) diff --git a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java index d3daf97f05..305a14407a 100644 --- a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java +++ b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java @@ -72,10 +72,10 @@ public class VaultQueryJavaTests { @Override public void recordTransactions(@NotNull Iterable txs) { for (SignedTransaction stx : txs) { - getStorageService().getValidatedTransactions().addTransaction(stx); + getValidatedTransactions().addTransaction(stx); } - Stream wtxn = StreamSupport.stream(txs.spliterator(), false).map(txn -> txn.getTx()); + Stream wtxn = StreamSupport.stream(txs.spliterator(), false).map(SignedTransaction::getTx); getVaultService().notifyAll(wtxn.collect(Collectors.toList())); } }; diff --git a/node/src/test/kotlin/net/corda/node/messaging/AttachmentTests.kt b/node/src/test/kotlin/net/corda/node/messaging/AttachmentTests.kt index 9331aeaf33..6baac37959 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/AttachmentTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/AttachmentTests.kt @@ -11,11 +11,10 @@ import net.corda.flows.FetchDataFlow import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.database.RequeryConfiguration import net.corda.node.services.network.NetworkMapService -import net.corda.node.services.persistence.NodeAttachmentService import net.corda.node.services.persistence.schemas.requery.AttachmentEntity import net.corda.node.services.transactions.SimpleNotaryService -import net.corda.testing.node.MockNetwork import net.corda.node.utilities.transaction +import net.corda.testing.node.MockNetwork import net.corda.testing.node.makeTestDataSourceProperties import org.jetbrains.exposed.sql.Database import org.junit.Before @@ -61,7 +60,7 @@ class AttachmentTests { // Insert an attachment into node zero's store directly. val id = n0.database.transaction { - n0.storage.attachments.importAttachment(ByteArrayInputStream(fakeAttachment())) + n0.attachments.importAttachment(ByteArrayInputStream(fakeAttachment())) } // Get node one to run a flow to fetch it and insert it. @@ -72,7 +71,7 @@ class AttachmentTests { // Verify it was inserted into node one's store. val attachment = n1.database.transaction { - n1.storage.attachments.openAttachment(id)!! + n1.attachments.openAttachment(id)!! } assertEquals(id, attachment.open().readBytes().sha256()) @@ -108,7 +107,7 @@ class AttachmentTests { return object : MockNetwork.MockNode(config, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) { override fun start(): MockNetwork.MockNode { super.start() - (storage.attachments as NodeAttachmentService).checkAttachmentsOnLoad = false + attachments.checkAttachmentsOnLoad = false return this } } @@ -119,7 +118,7 @@ class AttachmentTests { val attachment = fakeAttachment() // Insert an attachment into node zero's store directly. val id = n0.database.transaction { - n0.storage.attachments.importAttachment(ByteArrayInputStream(attachment)) + n0.attachments.importAttachment(ByteArrayInputStream(attachment)) } // Corrupt its store. @@ -130,7 +129,7 @@ class AttachmentTests { corruptAttachment.attId = id corruptAttachment.content = attachment n0.database.transaction { - (n0.storage.attachments as NodeAttachmentService).session.update(corruptAttachment) + n0.attachments.session.update(corruptAttachment) } diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index c8d8876680..2aebbe86b2 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -9,15 +9,21 @@ import net.corda.core.contracts.* import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sign -import net.corda.core.flows.* +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.InitiatedBy +import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.StateMachineRunId import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.internal.FlowStateMachine import net.corda.core.messaging.DataFeed import net.corda.core.messaging.SingleMessageRecipient +import net.corda.core.messaging.StateMachineTransactionMapping import net.corda.core.node.NodeInfo -import net.corda.core.node.services.* +import net.corda.core.node.services.ServiceInfo +import net.corda.core.node.services.TransactionStorage +import net.corda.core.node.services.Vault import net.corda.core.serialization.serialize import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder @@ -28,7 +34,6 @@ import net.corda.flows.TwoPartyTradeFlow.Seller import net.corda.node.internal.AbstractNode import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.persistence.DBTransactionStorage -import net.corda.node.services.persistence.StorageServiceImpl import net.corda.node.services.persistence.checkpoints import net.corda.node.utilities.transaction import net.corda.testing.* @@ -211,7 +216,7 @@ class TwoPartyTradeFlowTests { assertThat(bobNode.checkpointStorage.checkpoints()).hasSize(1) } - val storage = bobNode.storage.validatedTransactions + val storage = bobNode.services.validatedTransactions val bobTransactionsBeforeCrash = bobNode.database.transaction { (storage as DBTransactionStorage).transactions } @@ -252,7 +257,9 @@ class TwoPartyTradeFlowTests { } bobNode.database.transaction { - val restoredBobTransactions = bobTransactionsBeforeCrash.filter { bobNode.storage.validatedTransactions.getTransaction(it.id) != null } + val restoredBobTransactions = bobTransactionsBeforeCrash.filter { + bobNode.services.validatedTransactions.getTransaction(it.id) != null + } assertThat(restoredBobTransactions).containsAll(bobTransactionsBeforeCrash) } @@ -276,13 +283,9 @@ class TwoPartyTradeFlowTests { overrideServices: Map?, entropyRoot: BigInteger): MockNetwork.MockNode { return object : MockNetwork.MockNode(config, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) { - // That constructs the storage service object in a customised way ... - override fun constructStorageService( - attachments: AttachmentStorage, - transactionStorage: TransactionStorage, - stateMachineRecordedTransactionMappingStorage: StateMachineRecordedTransactionMappingStorage - ): StorageServiceImpl { - return StorageServiceImpl(attachments, RecordingTransactionStorage(database, transactionStorage), stateMachineRecordedTransactionMappingStorage) + // That constructs a recording tx storage + override fun createTransactionStorage(): TransactionStorage { + return RecordingTransactionStorage(database, super.createTransactionStorage()) } } } @@ -326,7 +329,7 @@ class TwoPartyTradeFlowTests { mockNet.runNetwork() run { - val records = (bobNode.storage.validatedTransactions as RecordingTransactionStorage).records + val records = (bobNode.services.validatedTransactions as RecordingTransactionStorage).records // Check Bobs's database accesses as Bob's cash transactions are downloaded by Alice. records.expectEvents(isStrict = false) { sequence( @@ -344,7 +347,7 @@ class TwoPartyTradeFlowTests { // Bob has downloaded the attachment. bobNode.database.transaction { - bobNode.storage.attachments.openAttachment(attachmentID)!!.openAsJAR().use { + bobNode.services.attachments.openAttachment(attachmentID)!!.openAsJAR().use { it.nextJarEntry val contents = it.reader().readText() assertTrue(contents.contains("Our commercial paper is top notch stuff")) @@ -354,7 +357,7 @@ class TwoPartyTradeFlowTests { // And from Alice's perspective ... run { - val records = (aliceNode.storage.validatedTransactions as RecordingTransactionStorage).records + val records = (aliceNode.services.validatedTransactions as RecordingTransactionStorage).records records.expectEvents(isStrict = false) { sequence( // Seller Alice sends her seller info to Bob, who wants to check the asset for sale. @@ -422,8 +425,10 @@ class TwoPartyTradeFlowTests { mockNet.runNetwork() // Clear network map registration messages - val aliceTxStream = aliceNode.storage.validatedTransactions.track().second - val aliceTxMappings = with(aliceNode) { database.transaction { storage.stateMachineRecordedTransactionMapping.track().second } } + val aliceTxStream = aliceNode.services.validatedTransactions.track().updates + val aliceTxMappings = with(aliceNode) { + database.transaction { services.stateMachineRecordedTransactionMapping.track().updates } + } val aliceSmId = runBuyerAndSeller(notaryNode, aliceNode, bobNode, "alice's paper".outputStateAndRef()).sellerId @@ -443,7 +448,7 @@ class TwoPartyTradeFlowTests { ) aliceTxStream.expectEvents { aliceTxExpectations } val aliceMappingExpectations = sequence( - expect { (stateMachineRunId, transactionId) -> + expect { (stateMachineRunId, transactionId) -> require(stateMachineRunId == aliceSmId) require(transactionId == bobsFakeCash[0].id) }, @@ -451,9 +456,9 @@ class TwoPartyTradeFlowTests { require(stateMachineRunId == aliceSmId) require(transactionId == bobsFakeCash[2].id) }, - expect { mapping: StateMachineTransactionMapping -> - require(mapping.stateMachineRunId == aliceSmId) - require(mapping.transactionId == bobsFakeCash[1].id) + expect { (stateMachineRunId, transactionId) -> + require(stateMachineRunId == aliceSmId) + require(transactionId == bobsFakeCash[1].id) } ) aliceTxMappings.expectEvents { aliceMappingExpectations } @@ -589,7 +594,7 @@ class TwoPartyTradeFlowTests { } return node.database.transaction { node.services.recordTransactions(signed) - val validatedTransactions = node.services.storageService.validatedTransactions + val validatedTransactions = node.services.validatedTransactions if (validatedTransactions is RecordingTransactionStorage) { validatedTransactions.records.clear() } diff --git a/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt b/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt index d82203d01a..406818e3c6 100644 --- a/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt +++ b/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt @@ -6,7 +6,6 @@ import net.corda.core.flows.FlowLogic import net.corda.core.node.NodeInfo import net.corda.core.node.services.* import net.corda.core.serialization.SerializeAsToken -import net.corda.core.transactions.SignedTransaction import net.corda.node.internal.InitiatedFlowFactory import net.corda.node.serialization.NodeClock import net.corda.node.services.api.* @@ -17,9 +16,11 @@ import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.services.statemachine.StateMachineManager import net.corda.node.services.transactions.InMemoryTransactionVerifierService import net.corda.testing.MOCK_IDENTITY_SERVICE +import net.corda.testing.node.MockAttachmentStorage import net.corda.testing.node.MockNetworkMapCache -import net.corda.testing.node.MockStorageService import org.jetbrains.exposed.sql.Database +import net.corda.testing.node.MockStateMachineRecordedTransactionMappingStorage +import net.corda.testing.node.MockTransactionStorage import java.time.Clock open class MockServiceHubInternal( @@ -28,13 +29,16 @@ open class MockServiceHubInternal( val keyManagement: KeyManagementService? = null, val network: MessagingService? = null, val identity: IdentityService? = MOCK_IDENTITY_SERVICE, - val storage: TxWritableStorageService? = MockStorageService(), + override val attachments: AttachmentStorage = MockAttachmentStorage(), + override val validatedTransactions: TransactionStorage = MockTransactionStorage(), + override val uploaders: List = listOf(), + override val stateMachineRecordedTransactionMapping: StateMachineRecordedTransactionMappingStorage = MockStateMachineRecordedTransactionMappingStorage(), val mapCache: NetworkMapCacheInternal? = null, val scheduler: SchedulerService? = null, val overrideClock: Clock? = NodeClock(), val schemas: SchemaService? = NodeSchemaService(), val customTransactionVerifierService: TransactionVerifierService? = InMemoryTransactionVerifierService(2) -) : ServiceHubInternal() { +) : ServiceHubInternal { override val vaultQueryService: VaultQueryService get() = customVaultQuery ?: throw UnsupportedOperationException() override val transactionVerifierService: TransactionVerifierService @@ -49,8 +53,6 @@ open class MockServiceHubInternal( get() = network ?: throw UnsupportedOperationException() override val networkMapCache: NetworkMapCacheInternal get() = mapCache ?: MockNetworkMapCache(this) - override val storageService: StorageService - get() = storage ?: throw UnsupportedOperationException() override val schedulerService: SchedulerService get() = scheduler ?: throw UnsupportedOperationException() override val clock: Clock @@ -67,14 +69,9 @@ open class MockServiceHubInternal( override val schemaService: SchemaService get() = schemas ?: throw UnsupportedOperationException() override val auditService: AuditService = DummyAuditService() - // We isolate the storage service with writable TXes so that it can't be accessed except via recordTransactions() - private val txStorageService: TxWritableStorageService - get() = storage ?: throw UnsupportedOperationException() lateinit var smm: StateMachineManager - override fun recordTransactions(txs: Iterable) = recordTransactionsInternal(txStorageService, txs) - override fun cordaService(type: Class): T = throw UnsupportedOperationException() override fun startFlow(logic: FlowLogic, flowInitiator: FlowInitiator): FlowStateMachineImpl { diff --git a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt index 44486779d1..866a7cb3d2 100644 --- a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt @@ -99,7 +99,7 @@ class NotaryChangeTests { val newState = future.resultFuture.getOrThrow() assertEquals(newState.state.notary, newNotary) - val notaryChangeTx = clientNodeA.services.storageService.validatedTransactions.getTransaction(newState.ref.txhash)!!.tx + val notaryChangeTx = clientNodeA.services.validatedTransactions.getTransaction(newState.ref.txhash)!!.tx // Check that all encumbrances have been propagated to the outputs val originalOutputs = issueTx.outputs.map { it.data } diff --git a/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt index cebee41ce1..4cf8b2746b 100644 --- a/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt @@ -85,7 +85,7 @@ class HibernateConfigurationTest { override fun recordTransactions(txs: Iterable) { for (stx in txs) { - storageService.validatedTransactions.addTransaction(stx) + validatedTransactions.addTransaction(stx) } // Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions. vaultService.notifyAll(txs.map { it.tx }) diff --git a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt index a4164e3008..7a0ec5171e 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt @@ -6,7 +6,6 @@ import net.corda.contracts.testing.fillWithSomeTestCash import net.corda.core.contracts.* import net.corda.core.identity.AnonymousParty import net.corda.core.node.services.StatesNotAvailableException -import net.corda.core.node.services.TxWritableStorageService import net.corda.core.node.services.VaultService import net.corda.core.node.services.unconsumedStates import net.corda.core.serialization.OpaqueBytes @@ -53,7 +52,7 @@ class NodeVaultServiceTest { override fun recordTransactions(txs: Iterable) { for (stx in txs) { - storageService.validatedTransactions.addTransaction(stx) + validatedTransactions.addTransaction(stx) } // Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions. vaultService.notifyAll(txs.map { it.tx }) @@ -77,17 +76,12 @@ class NodeVaultServiceTest { val w1 = vaultSvc.unconsumedStates() assertThat(w1).hasSize(3) - val originalStorage = services.storageService val originalVault = vaultSvc val services2 = object : MockServices() { override val vaultService: VaultService get() = originalVault - - // We need to be able to find the same transactions as before, too. - override val storageService: TxWritableStorageService get() = originalStorage - override fun recordTransactions(txs: Iterable) { for (stx in txs) { - storageService.validatedTransactions.addTransaction(stx) + validatedTransactions.addTransaction(stx) vaultService.notify(stx.tx) } } diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index 6db977b677..3183a5546a 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -75,7 +75,7 @@ class VaultQueryTests { override fun recordTransactions(txs: Iterable) { for (stx in txs) { - storageService.validatedTransactions.addTransaction(stx) + validatedTransactions.addTransaction(stx) } // Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions. vaultService.notifyAll(txs.map { it.tx }) @@ -93,9 +93,9 @@ class VaultQueryTests { /** * Helper method for generating a Persistent H2 test database */ - @Ignore //@Test + @Ignore + @Test fun createPersistentTestDb() { - val dataSourceAndDatabase = configureDatabase(makePersistentDataSourceProperties()) val dataSource = dataSourceAndDatabase.first val database = dataSourceAndDatabase.second diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt index 7714a2186b..5eb53717d2 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt @@ -3,7 +3,9 @@ package net.corda.node.services.vault import net.corda.contracts.DummyDealContract import net.corda.contracts.asset.Cash import net.corda.contracts.asset.DUMMY_CASH_ISSUER -import net.corda.contracts.testing.* +import net.corda.contracts.testing.fillWithSomeTestCash +import net.corda.contracts.testing.fillWithSomeTestDeals +import net.corda.contracts.testing.fillWithSomeTestLinearStates import net.corda.core.contracts.* import net.corda.core.identity.AnonymousParty import net.corda.core.node.services.VaultService @@ -54,7 +56,7 @@ class VaultWithCashTest { override fun recordTransactions(txs: Iterable) { for (stx in txs) { - storageService.validatedTransactions.addTransaction(stx) + validatedTransactions.addTransaction(stx) } // Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions. vaultService.notifyAll(txs.map { it.tx }) diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/BuyerFlow.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/BuyerFlow.kt index 53024ad9a5..1bcc64ef8b 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/BuyerFlow.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/BuyerFlow.kt @@ -55,14 +55,14 @@ class BuyerFlow(val otherParty: Party) : FlowLogic() { private fun logIssuanceAttachment(tradeTX: SignedTransaction) { // Find the original CP issuance. - val search = TransactionGraphSearch(serviceHub.storageService.validatedTransactions, listOf(tradeTX.tx)) + val search = TransactionGraphSearch(serviceHub.validatedTransactions, listOf(tradeTX.tx)) search.query = TransactionGraphSearch.Query(withCommandOfType = CommercialPaper.Commands.Issue::class.java, followInputsOfType = CommercialPaper.State::class.java) val cpIssuance = search.call().single() // Buyer will fetch the attachment from the seller automatically when it resolves the transaction. // For demo purposes just extract attachment jars when saved to disk, so the user can explore them. - val attachmentsPath = (serviceHub.storageService.attachments).let { + val attachmentsPath = (serviceHub.attachments).let { it.automaticallyExtractAttachments = true it.storePath } diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt index 3a7ecba1d7..7d0a633e70 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt @@ -79,7 +79,7 @@ class SellerFlow(val otherParty: Party, // TODO: Consider moving these two steps below into generateIssue. // Attach the prospectus. - tx.addAttachment(serviceHub.storageService.attachments.openAttachment(PROSPECTUS_HASH)!!.id) + tx.addAttachment(serviceHub.attachments.openAttachment(PROSPECTUS_HASH)!!.id) // Requesting a time-window to be set, all CP must have a validation window. tx.addTimeWindow(Instant.now(), 30.seconds) diff --git a/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt b/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt index 7a0e41094b..4eb3669952 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt @@ -211,8 +211,9 @@ data class TestLedgerDSLInterpreter private constructor( } } - internal fun resolveAttachment(attachmentId: SecureHash): Attachment = - services.storageService.attachments.openAttachment(attachmentId) ?: throw AttachmentResolutionException(attachmentId) + internal fun resolveAttachment(attachmentId: SecureHash): Attachment { + return services.attachments.openAttachment(attachmentId) ?: throw AttachmentResolutionException(attachmentId) + } private fun interpretTransactionDsl( transactionBuilder: TransactionBuilder, @@ -276,7 +277,7 @@ data class TestLedgerDSLInterpreter private constructor( dsl(LedgerDSL(copy())) override fun attachment(attachment: InputStream): SecureHash { - return services.storageService.attachments.importAttachment(attachment) + return services.attachments.importAttachment(attachment) } override fun verifies(): EnforceVerifyOrFail { diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt index 1fdd752414..30fe59eadc 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -6,7 +6,6 @@ import net.corda.core.crypto.* import net.corda.core.flows.StateMachineRunId import net.corda.core.identity.PartyAndCertificate import net.corda.core.messaging.DataFeed -import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub import net.corda.core.node.services.* @@ -16,6 +15,7 @@ import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.DUMMY_CA import net.corda.core.utilities.getTestPartyAndCertificate import net.corda.flows.AnonymisedIdentity +import net.corda.node.services.api.StateMachineRecordedTransactionMappingStorage import net.corda.node.services.database.HibernateConfiguration import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.keys.freshCertificate @@ -28,7 +28,6 @@ import net.corda.node.services.vault.NodeVaultService import net.corda.testing.MEGA_CORP import net.corda.testing.MOCK_IDENTITIES import net.corda.testing.MOCK_VERSION_INFO -import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.operator.ContentSigner import rx.Observable import rx.subjects.PublishSubject @@ -41,11 +40,9 @@ import java.nio.file.Paths import java.security.KeyPair import java.security.PrivateKey import java.security.PublicKey -import java.security.cert.CertPath import java.time.Clock import java.util.* import java.util.jar.JarInputStream -import javax.annotation.concurrent.ThreadSafe // TODO: We need a single, rationalised unit testing environment that is usable for everything. Fix this! // That means it probably shouldn't be in the 'core' module, which lacks enough code to create a realistic test env. @@ -61,14 +58,16 @@ open class MockServices(vararg val keys: KeyPair) : ServiceHub { override fun recordTransactions(txs: Iterable) { txs.forEach { - storageService.stateMachineRecordedTransactionMapping.addMapping(StateMachineRunId.createRandom(), it.id) + stateMachineRecordedTransactionMapping.addMapping(StateMachineRunId.createRandom(), it.id) } for (stx in txs) { - storageService.validatedTransactions.addTransaction(stx) + validatedTransactions.addTransaction(stx) } } - override val storageService: TxWritableStorageService = MockStorageService() + override val attachments: AttachmentStorage = MockAttachmentStorage() + override val validatedTransactions: TransactionStorage = MockTransactionStorage() + val stateMachineRecordedTransactionMapping: StateMachineRecordedTransactionMappingStorage = MockStateMachineRecordedTransactionMappingStorage() override final val identityService: IdentityService = InMemoryIdentityService(MOCK_IDENTITIES, trustRoot = DUMMY_CA.certificate) override val keyManagementService: KeyManagementService = MockKeyManagementService(identityService, *keys) @@ -185,15 +184,6 @@ open class MockTransactionStorage : TransactionStorage { override fun getTransaction(id: SecureHash): SignedTransaction? = txns[id] } -@ThreadSafe -class MockStorageService(override val attachments: AttachmentStorage = MockAttachmentStorage(), - override val validatedTransactions: TransactionStorage = MockTransactionStorage(), - override val uploaders: List = listOf(), - override val stateMachineRecordedTransactionMapping: StateMachineRecordedTransactionMappingStorage = MockStateMachineRecordedTransactionMappingStorage()) - : SingletonSerializeAsToken(), TxWritableStorageService { - override val attachmentsClassLoaderEnabled = false -} - /** * Make properties appropriate for creating a DataSource for unit tests. * From 88c8f4b351778c4328f46af08fe17514535fa4d9 Mon Sep 17 00:00:00 2001 From: Andrzej Cichocki Date: Fri, 30 Jun 2017 10:52:24 +0100 Subject: [PATCH 28/97] Avoid BFT printStackTraces when cluster is starting up (#899) --- .../node/services/transactions/BFTSMaRt.kt | 7 ++-- .../services/transactions/BFTSMaRtConfig.kt | 34 ++++++++++++++++++- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt b/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt index d95f768a4a..f910c0ef1e 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt @@ -179,8 +179,11 @@ object BFTSMaRt { // TODO: Use Requery with proper DB schema instead of JDBCHashMap. // Must be initialised before ServiceReplica is started private val commitLog = services.database.transaction { JDBCHashMap(tableName) } - @Suppress("LeakingThis") - private val replica = CordaServiceReplica(replicaId, config.path, this) + private val replica = run { + config.waitUntilReplicaWillNotPrintStackTrace(replicaId) + @Suppress("LeakingThis") + CordaServiceReplica(replicaId, config.path, this) + } fun dispose() { replica.dispose() diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRtConfig.kt b/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRtConfig.kt index 0643e4aa6a..c11952c202 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRtConfig.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRtConfig.kt @@ -2,10 +2,15 @@ package net.corda.node.services.transactions import com.google.common.net.HostAndPort import net.corda.core.div +import net.corda.core.utilities.debug +import net.corda.core.utilities.loggerFor import java.io.FileWriter import java.io.PrintWriter import java.net.InetAddress +import java.net.Socket +import java.net.SocketException import java.nio.file.Files +import java.util.concurrent.TimeUnit.MILLISECONDS /** * BFT SMaRt can only be configured via files in a configHome directory. @@ -14,6 +19,7 @@ import java.nio.file.Files */ class BFTSMaRtConfig(private val replicaAddresses: List, debug: Boolean = false) : PathManager(Files.createTempDirectory("bft-smart-config")) { companion object { + private val log = loggerFor() internal val portIsClaimedFormat = "Port %s is claimed by another replica: %s" } @@ -47,12 +53,38 @@ class BFTSMaRtConfig(private val replicaAddresses: List, debug: Boo } } + fun waitUntilReplicaWillNotPrintStackTrace(contextReplicaId: Int) { + // A replica will printStackTrace until all lower-numbered replicas are listening. + // But we can't probe a replica without it logging EOFException when our probe succeeds. + // So to keep logging to a minimum we only check the previous replica: + val peerId = contextReplicaId - 1 + if (peerId < 0) return + // The printStackTrace we want to avoid is in replica-replica communication code: + val address = BFTSMaRtPort.FOR_REPLICAS.ofReplica(replicaAddresses[peerId]) + log.debug { "Waiting for replica $peerId to start listening on: $address" } + while (!address.isListening()) MILLISECONDS.sleep(200) + log.debug { "Replica $peerId is ready for P2P." } + } + private fun replicaPorts(replicaId: Int): List { val base = replicaAddresses[replicaId] - return (0..1).map { HostAndPort.fromParts(base.host, base.port + it) } + return BFTSMaRtPort.values().map { it.ofReplica(base) } } } +private enum class BFTSMaRtPort(private val off: Int) { + FOR_CLIENTS(0), + FOR_REPLICAS(1); + + fun ofReplica(base: HostAndPort) = HostAndPort.fromParts(base.host, base.port + off) +} + +private fun HostAndPort.isListening() = try { + Socket(host, port).use { true } // Will cause one error to be logged in the replica on success. +} catch (e: SocketException) { + false +} + fun maxFaultyReplicas(clusterSize: Int) = (clusterSize - 1) / 3 fun minCorrectReplicas(clusterSize: Int) = (2 * clusterSize + 3) / 3 fun minClusterSize(maxFaultyReplicas: Int) = maxFaultyReplicas * 3 + 1 From 1c2265d3b7968b24bc3bff2f972e0c14f28922c0 Mon Sep 17 00:00:00 2001 From: Patrick Kuo Date: Fri, 30 Jun 2017 11:45:08 +0100 Subject: [PATCH 29/97] Pat remove stability test randomness (#891) * Remove randomness from stability test * address PR issues --- .../corda/loadtest/LoadTestConfiguration.kt | 2 +- .../main/kotlin/net/corda/loadtest/Main.kt | 10 ++-- .../net/corda/loadtest/tests/StabilityTest.kt | 47 +++++++------------ 3 files changed, 24 insertions(+), 35 deletions(-) diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/LoadTestConfiguration.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/LoadTestConfiguration.kt index 8cf9b6568b..8e4a962002 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/LoadTestConfiguration.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/LoadTestConfiguration.kt @@ -33,7 +33,7 @@ data class LoadTestConfiguration( val remoteSystemdServiceName: String, val seed: Long?, val mode: TestMode = TestMode.LOAD_TEST, - val executionFrequency: Int = 20, + val executionFrequency: Int = 2, val generateCount: Int = 10000, val parallelism: Int = ForkJoinPool.getCommonPoolParallelism()) diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/Main.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/Main.kt index 0bce7c7f44..4a9c59d780 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/Main.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/Main.kt @@ -133,17 +133,17 @@ private fun runLoadTest(loadTestConfiguration: LoadTestConfiguration) { private fun runStabilityTest(loadTestConfiguration: LoadTestConfiguration) { runLoadTests(loadTestConfiguration, listOf( - // Self issue cash. - StabilityTest.selfIssueTest to LoadTest.RunParameters( + // Self issue cash. This is a pre test step to make sure vault have enough cash to work with. + StabilityTest.selfIssueTest(100) to LoadTest.RunParameters( parallelism = loadTestConfiguration.parallelism, - generateCount = loadTestConfiguration.generateCount, + generateCount = 1000, clearDatabaseBeforeRun = false, - executionFrequency = loadTestConfiguration.executionFrequency, + executionFrequency = 50, gatherFrequency = 100, disruptionPatterns = listOf(listOf()) // no disruptions ), // Send cash to a random party or exit cash, commands are generated randomly. - StabilityTest.crossCashTest to LoadTest.RunParameters( + StabilityTest.crossCashTest(100) to LoadTest.RunParameters( parallelism = loadTestConfiguration.parallelism, generateCount = loadTestConfiguration.generateCount, clearDatabaseBeforeRun = false, diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/StabilityTest.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/StabilityTest.kt index 08c8f48766..f15e073733 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/StabilityTest.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/StabilityTest.kt @@ -1,29 +1,26 @@ package net.corda.loadtest.tests import net.corda.client.mock.Generator -import net.corda.client.mock.pickOne -import net.corda.client.mock.replicatePoisson +import net.corda.core.contracts.Amount import net.corda.core.contracts.USD import net.corda.core.failure import net.corda.core.flows.FlowException import net.corda.core.getOrThrow +import net.corda.core.serialization.OpaqueBytes import net.corda.core.success import net.corda.core.utilities.loggerFor +import net.corda.flows.CashFlowCommand import net.corda.loadtest.LoadTest object StabilityTest { private val log = loggerFor() - val crossCashTest = LoadTest( - "Creating Cash transactions randomly", + fun crossCashTest(replication: Int) = LoadTest( + "Creating Cash transactions", generate = { _, _ -> - val nodeMap = simpleNodes.associateBy { it.info.legalIdentity } - Generator.sequence(simpleNodes.map { node -> - val possibleRecipients = nodeMap.keys.toList() - val moves = 0.5 to generateMove(1, USD, node.info.legalIdentity, possibleRecipients, anonymous = true) - val exits = 0.5 to generateExit(1, USD) - val command = Generator.frequency(listOf(moves, exits)) - command.map { CrossCashCommand(it, nodeMap[node.info.legalIdentity]!!) } - }) + val payments = simpleNodes.flatMap { payer -> simpleNodes.map { payer to it } } + .filter { it.first != it.second } + .map { (payer, payee) -> CrossCashCommand(CashFlowCommand.PayCash(Amount(1, USD), payee.info.legalIdentity, anonymous = true), payer) } + Generator.pure(List(replication) { payments }.flatten()) }, interpret = { _, _ -> }, execute = { command -> @@ -38,24 +35,16 @@ object StabilityTest { gatherRemoteState = {} ) - val selfIssueTest = LoadTest( - "Self issuing cash randomly", - generate = { _, parallelism -> - val generateIssue = Generator.pickOne(simpleNodes).bind { node -> - generateIssue(1000, USD, notary.info.notaryIdentity, listOf(node.info.legalIdentity), anonymous = true).map { - SelfIssueCommand(it, node) - } - } - Generator.replicatePoisson(parallelism.toDouble(), generateIssue).bind { - // We need to generate at least one - if (it.isEmpty()) { - Generator.sequence(listOf(generateIssue)) - } else { - Generator.pure(it) - } - } + fun selfIssueTest(replication: Int) = LoadTest( + "Self issuing lot of cash", + generate = { _, _ -> + // Self issue cash is fast, its ok to flood the node with this command. + val generateIssue = + simpleNodes.map { issuer -> + SelfIssueCommand(CashFlowCommand.IssueCash(Amount(100000, USD), OpaqueBytes.of(0), issuer.info.legalIdentity, notary.info.notaryIdentity, anonymous = true), issuer) + } + Generator.pure(List(replication) { generateIssue }.flatten()) }, - interpret = { _, _ -> }, execute = { command -> try { From 82f68f212a40898585c6ca555a1af0b26a1ec544 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Fri, 30 Jun 2017 11:34:00 +0100 Subject: [PATCH 30/97] Minor: add another emoji, import a couple of changes for Enterprise --- core/src/main/kotlin/net/corda/core/utilities/Emoji.kt | 2 ++ .../src/main/groovy/net/corda/plugins/Node.groovy | 2 +- node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) 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 bc7fd86a14..f2a6a6b566 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/Emoji.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/Emoji.kt @@ -28,6 +28,7 @@ object Emoji { @JvmStatic val CODE_SKULL_AND_CROSSBONES: String = codePointsString(0x2620) @JvmStatic val CODE_BOOKS: String = codePointsString(0x1F4DA) @JvmStatic val CODE_SLEEPING_FACE: String = codePointsString(0x1F634) + @JvmStatic val CODE_LIGHTBULB: String = codePointsString(0x1F4A1) /** * When non-null, toString() methods are allowed to use emoji in the output as we're going to render them to a @@ -44,6 +45,7 @@ object Emoji { val coolGuy: String get() = if (emojiMode.get() != null) "$CODE_COOL_GUY " else "" val books: String get() = if (emojiMode.get() != null) "$CODE_BOOKS " else "" val sleepingFace: String get() = if (emojiMode.get() != null) "$CODE_SLEEPING_FACE " else "" + val lightBulb: String get() = if (emojiMode.get() != null) "$CODE_LIGHTBULB " else "" // These have old/non-emoji symbols with better platform support. val greenTick: String get() = if (emojiMode.get() != null) "$CODE_GREEN_TICK " else "✓" diff --git a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy index 3f3a53c8f2..32038740cd 100644 --- a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy +++ b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy @@ -218,7 +218,7 @@ class Node extends CordformNode { */ private File verifyAndGetCordaJar() { def maybeCordaJAR = project.configurations.runtime.filter { - it.toString().contains("corda-${project.corda_release_version}.jar") + it.toString().contains("corda-${project.corda_release_version}.jar") || it.toString().contains("corda-enterprise-${project.corda_release_version}.jar") } if (maybeCordaJAR.size() == 0) { throw new RuntimeException("No Corda Capsule JAR found. Have you deployed the Corda project to Maven? Looked for \"corda-${project.corda_release_version}.jar\"") diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt index 937e61d18e..2251244e0b 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -72,7 +72,7 @@ open class NodeStartup(val args: Array) { Node.printBasicNodeInfo(LOGS_CAN_BE_FOUND_IN_STRING, System.getProperty("log-path")) val conf = loadConfigFile(cmdlineOptions) banJavaSerialisation(conf) - preNetworkRegistration() + preNetworkRegistration(conf) maybeRegisterWithNetworkAndExit(cmdlineOptions, conf) logStartupInfo(versionInfo, cmdlineOptions, conf) @@ -91,7 +91,7 @@ open class NodeStartup(val args: Array) { exitProcess(0) } - open protected fun preNetworkRegistration() = Unit + open protected fun preNetworkRegistration(conf: FullNodeConfiguration) = Unit open protected fun createNode(conf: FullNodeConfiguration, versionInfo: VersionInfo, services: Set): Node { return Node(conf, services, versionInfo, if (conf.useTestClock) TestClock() else NodeClock()) From 04d7432622f43a7962f7379c93d484ac18fccc7a Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Fri, 30 Jun 2017 14:12:36 +0100 Subject: [PATCH 31/97] Add secondry deserializer to return amqp envelope Useful for testing the carpenter by giving access to a cosntructed envelope rather than manually building one every time --- .../amqp/DeserializationInput.kt | 73 ++++++++++++++----- .../amqp/DeserializeAndReturnEnvelope.kt | 42 +++++++++++ 2 files changed, 95 insertions(+), 20 deletions(-) create mode 100644 core/src/test/kotlin/net/corda/core/serialization/amqp/DeserializeAndReturnEnvelope.kt 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 3258375fad..bcb168e22d 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 @@ -11,6 +11,9 @@ import java.lang.reflect.Type import java.nio.ByteBuffer import java.util.* + +data class classAndEnvelope(val obj: T, val envelope: Envelope) + /** * Main entry point for deserializing an AMQP encoded object. * @@ -57,7 +60,44 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory = S } @Throws(NotSerializableException::class) - inline fun deserialize(bytes: SerializedBytes): T = deserialize(bytes, T::class.java) + inline fun deserialize(bytes: SerializedBytes): T = + deserialize(bytes, T::class.java) + + + @Throws(NotSerializableException::class) + inline fun deserializeAndReturnEnvelope(bytes: SerializedBytes): classAndEnvelope = + deserializeAndReturnEnvelope(bytes, T::class.java) + + + @Throws(NotSerializableException::class) + fun getEnvelope(bytes: SerializedBytes): Envelope { + // Check that the lead bytes match expected header + if (!subArraysEqual(bytes.bytes, 0, 8, AmqpHeaderV1_0.bytes, 0)) { + throw NotSerializableException("Serialization header does not match.") + } + + val data = Data.Factory.create() + val size = data.decode(ByteBuffer.wrap(bytes.bytes, 8, bytes.size - 8)) + if (size.toInt() != bytes.size - 8) { + throw NotSerializableException("Unexpected size of data") + } + + return Envelope.get(data) + } + + + @Throws(NotSerializableException::class) + fun des(bytes: SerializedBytes, clazz: Class, generator: (SerializedBytes, Class) -> R): R { + try { + return generator(bytes, clazz) + } catch(nse: NotSerializableException) { + throw nse + } catch(t: Throwable) { + throw NotSerializableException("Unexpected throwable: ${t.message} ${Throwables.getStackTraceAsString(t)}") + } finally { + objectHistory.clear() + } + } /** * This is the main entry point for deserialization of AMQP payloads, and expects a byte sequence involving a header @@ -66,25 +106,18 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory = S */ @Throws(NotSerializableException::class) fun deserialize(bytes: SerializedBytes, clazz: Class): T { - try { - // Check that the lead bytes match expected header - if (!subArraysEqual(bytes.bytes, 0, 8, AmqpHeaderV1_0.bytes, 0)) { - throw NotSerializableException("Serialization header does not match.") - } - val data = Data.Factory.create() - val size = data.decode(ByteBuffer.wrap(bytes.bytes, 8, bytes.size - 8)) - if (size.toInt() != bytes.size - 8) { - throw NotSerializableException("Unexpected size of data") - } - val envelope = Envelope.get(data) + return des(bytes, clazz) { bytes, clazz -> + var envelope = getEnvelope(bytes) + clazz.cast(readObjectOrNull(envelope.obj, envelope.schema, clazz)) + } + } + + @Throws(NotSerializableException::class) + fun deserializeAndReturnEnvelope(bytes: SerializedBytes, clazz: Class): classAndEnvelope { + return des>(bytes, clazz) { bytes, clazz -> + val envelope = getEnvelope(bytes) // Now pick out the obj and schema from the envelope. - return clazz.cast(readObjectOrNull(envelope.obj, envelope.schema, clazz)) - } catch(nse: NotSerializableException) { - throw nse - } catch(t: Throwable) { - throw NotSerializableException("Unexpected throwable: ${t.message} ${Throwables.getStackTraceAsString(t)}") - } finally { - objectHistory.clear() + classAndEnvelope(clazz.cast(readObjectOrNull(envelope.obj, envelope.schema, clazz)), envelope) } } @@ -109,4 +142,4 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory = S return obj } } -} \ No newline at end of file +} diff --git a/core/src/test/kotlin/net/corda/core/serialization/amqp/DeserializeAndReturnEnvelope.kt b/core/src/test/kotlin/net/corda/core/serialization/amqp/DeserializeAndReturnEnvelope.kt new file mode 100644 index 0000000000..16dad3f5a2 --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/serialization/amqp/DeserializeAndReturnEnvelope.kt @@ -0,0 +1,42 @@ +package net.corda.core.serialization.amqp + +import org.junit.Test +import kotlin.test.* + +class DeserializeAndReturnEnvelope { + + fun testName() = Thread.currentThread().stackTrace[2].methodName + inline fun classTestName(clazz: String) = "${this.javaClass.name}\$${testName()}\$$clazz" + + @Test + fun oneType() { + data class A(val a: Int, val b: String) + + val a = A(10, "20") + + var factory = SerializerFactory() + fun serialise(clazz: Any) = SerializationOutput(factory).serialize(clazz) + val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) + + assertTrue(obj.obj is A) + assertEquals(1, obj.envelope.schema.types.size) + assertEquals(classTestName("A"), obj.envelope.schema.types.first().name) + } + + @Test + fun twoTypes() { + data class A(val a: Int, val b: String) + data class B(val a: A, val b: Float) + + val b = B(A(10, "20"), 30.0F) + + var factory = SerializerFactory() + fun serialise(clazz: Any) = SerializationOutput(factory).serialize(clazz) + val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b)) + + assertTrue(obj.obj is B) + assertEquals(2, obj.envelope.schema.types.size) + assertNotEquals(null, obj.envelope.schema.types.find { it.name == classTestName("A") }) + assertNotEquals(null, obj.envelope.schema.types.find { it.name == classTestName("B") }) + } +} From c1088038b759b05de6838a1432f48746e6bb8351 Mon Sep 17 00:00:00 2001 From: Clinton Date: Fri, 30 Jun 2017 14:18:46 +0100 Subject: [PATCH 32/97] Cordapps now contain their own dependencies (#915) * Cordapps now contain all explicitly specified dependencies (and sub dependencies). * Removed some useless compile dependencies for trader demo. * Dependent Cordapps are excluded from the build. :Removed unnecessary dependencies of demos. * Cleaned up exclusion rules for cordapp dependencies. --- client/rpc/build.gradle | 2 +- constants.properties | 2 +- docs/source/tutorial-cordapp.rst | 7 +--- .../net/corda/plugins/Cordformation.groovy | 38 +++++++++++++++++++ .../main/groovy/net/corda/plugins/Node.groovy | 18 --------- node/src/main/java/CordaCaplet.java | 3 +- samples/attachment-demo/build.gradle | 9 ----- samples/network-visualiser/build.gradle | 3 -- samples/notary-demo/build.gradle | 3 -- samples/simm-valuation-demo/build.gradle | 3 -- samples/trader-demo/build.gradle | 14 +------ 11 files changed, 43 insertions(+), 59 deletions(-) diff --git a/client/rpc/build.gradle b/client/rpc/build.gradle index 7ff552073d..2a7a682cda 100644 --- a/client/rpc/build.gradle +++ b/client/rpc/build.gradle @@ -36,7 +36,7 @@ sourceSets { } processSmokeTestResources { - from(project(':node:capsule').tasks.buildCordaJAR) { + from(project(':node:capsule').tasks['buildCordaJAR']) { rename 'corda-(.*)', 'corda.jar' } } diff --git a/constants.properties b/constants.properties index e1c47af61a..eebbf86a14 100644 --- a/constants.properties +++ b/constants.properties @@ -1,4 +1,4 @@ -gradlePluginsVersion=0.13.0 +gradlePluginsVersion=0.13.1 kotlinVersion=1.1.1 guavaVersion=21.0 bouncycastleVersion=1.57 diff --git a/docs/source/tutorial-cordapp.rst b/docs/source/tutorial-cordapp.rst index 97ee76af97..c17c81b874 100644 --- a/docs/source/tutorial-cordapp.rst +++ b/docs/source/tutorial-cordapp.rst @@ -262,22 +262,18 @@ folder has the following structure: . nodes ├── controller │   ├── corda.jar - │   ├── dependencies │   ├── node.conf │   └── plugins ├── nodea │   ├── corda.jar - │   ├── dependencies │   ├── node.conf │   └── plugins ├── nodeb │   ├── corda.jar - │   ├── dependencies │   ├── node.conf │   └── plugins ├── nodec │   ├── corda.jar - │   ├── dependencies │   ├── node.conf │   └── plugins ├── runnodes @@ -286,7 +282,7 @@ folder has the following structure: There will be one folder generated for each node you build (more on later when we get into the detail of the ``deployNodes`` Gradle task) and a ``runnodes`` shell script (batch file on Windows). -Each node folder contains the Corda JAR, a folder for dependencies and a folder for plugins (or CorDapps). There is also +Each node folder contains the Corda JAR and a folder for plugins (or CorDapps). There is also a node.conf file. See :doc:`Corda configuration files `. **Building from IntelliJ** @@ -340,7 +336,6 @@ When booted up, the node will generate a bunch of files and directories in addit ├── cache ├── certificates ├── corda.jar - ├── dependencies ├── identity-private-key ├── identity-public ├── logs diff --git a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy index 3151343f9a..6d41b9d370 100644 --- a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy +++ b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy @@ -13,6 +13,21 @@ class Cordformation implements Plugin { Configuration cordappConf = project.configurations.create("cordapp") cordappConf.transitive = false project.configurations.compile.extendsFrom cordappConf + + configureCordappJar(project) + } + + /** + * Configures this project's JAR as a Cordapp JAR + */ + private void configureCordappJar(Project project) { + // Note: project.afterEvaluate did not have full dependency resolution completed, hence a task is used instead + def task = project.task('configureCordappFatJar') { + doLast { + project.tasks.jar.from getDirectNonCordaDependencies(project).collect { project.zipTree(it) }.flatten() + } + } + project.tasks.jar.dependsOn task } /** @@ -27,4 +42,27 @@ class Cordformation implements Plugin { it.name.contains('cordformation') }, filePathInJar).asFile() } + + static def getDirectNonCordaDependencies(Project project) { + def coreCordaNames = ['jfx', 'mock', 'rpc', 'core', 'corda', 'cordform-common', 'corda-webserver', 'finance', 'node', 'node-api', 'node-schemas', 'test-utils', 'jackson', 'verifier', 'webserver', 'capsule', 'webcapsule'] + def excludes = coreCordaNames.collect { [group: 'net.corda', name: it] } + [ + [group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib'], + [group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jre8'], + [group: 'co.paralleluniverse', name: 'quasar-core'] + ] + // The direct dependencies of this project + def cordappDeps = project.configurations.cordapp.allDependencies + def directDeps = project.configurations.runtime.allDependencies - cordappDeps + // We want to filter out anything Corda related or provided by Corda, like kotlin-stdlib and quasar + def filteredDeps = directDeps.findAll { excludes.collect { exclude -> (exclude.group == it.group) && (exclude.name == it.name) }.findAll { it }.isEmpty() } + filteredDeps.each { + // net.corda may be a core dependency which shouldn't be included in this cordapp so give a warning + if(it.group.contains('net.corda')) { + project.logger.warn("Including a dependency with a net.corda group: $it") + } else { + project.logger.trace("Including dependency: $it") + } + } + return filteredDeps.collect { project.configurations.runtime.files it }.flatten().toSet() + } } diff --git a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy index 32038740cd..fb2ea88550 100644 --- a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy +++ b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy @@ -104,7 +104,6 @@ class Node extends CordformNode { installWebserverJar() installBuiltPlugin() installCordapps() - installDependencies() installConfig() } @@ -172,23 +171,6 @@ class Node extends CordformNode { } } - /** - * Installs other dependencies to this node's dependencies directory. - */ - private void installDependencies() { - def cordaJar = verifyAndGetCordaJar() - def webJar = verifyAndGetWebserverJar() - def depsDir = new File(nodeDir, "dependencies") - def coreDeps = project.zipTree(cordaJar).getFiles().collect { it.getName() } - def appDeps = project.configurations.runtime.filter { - (it != cordaJar) && (it != webJar) && !project.configurations.cordapp.contains(it) && !coreDeps.contains(it.getName()) - } - project.copy { - from appDeps - into depsDir - } - } - /** * Installs the configuration file to this node's directory and detokenises it. */ diff --git a/node/src/main/java/CordaCaplet.java b/node/src/main/java/CordaCaplet.java index de53b6ff08..83e76ae2ba 100644 --- a/node/src/main/java/CordaCaplet.java +++ b/node/src/main/java/CordaCaplet.java @@ -24,8 +24,7 @@ public class CordaCaplet extends Capsule { // defined as public static final fields on the Capsule class, therefore referential equality is safe. if (ATTR_APP_CLASS_PATH == attr) { T cp = super.attribute(attr); - List classpath = augmentClasspath((List) cp, "plugins"); - return (T) augmentClasspath(classpath, "dependencies"); + return (T) augmentClasspath((List) cp, "plugins"); } return super.attribute(attr); } diff --git a/samples/attachment-demo/build.gradle b/samples/attachment-demo/build.gradle index 936586894e..55be04795d 100644 --- a/samples/attachment-demo/build.gradle +++ b/samples/attachment-demo/build.gradle @@ -30,15 +30,6 @@ dependencies { compile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') compile project(':core') compile project(':test-utils') - - // Javax is required for webapis - compile "org.glassfish.jersey.core:jersey-server:${jersey_version}" - - // GraphStream: For visualisation (required by ExampleClientRPC app) - compile "org.graphstream:gs-core:1.3" - compile("org.graphstream:gs-ui:1.3") { - exclude group: "bouncycastle" - } } task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { diff --git a/samples/network-visualiser/build.gradle b/samples/network-visualiser/build.gradle index 86337f2baa..7450bcd816 100644 --- a/samples/network-visualiser/build.gradle +++ b/samples/network-visualiser/build.gradle @@ -18,9 +18,6 @@ dependencies { compile project(':core') compile project(':finance') - // Javax is required for webapis - compile "org.glassfish.jersey.core:jersey-server:${jersey_version}" - // Cordapp dependencies // GraphStream: For visualisation compile 'co.paralleluniverse:capsule:1.0.3' diff --git a/samples/notary-demo/build.gradle b/samples/notary-demo/build.gradle index 938b38cfd4..5f9204b252 100644 --- a/samples/notary-demo/build.gradle +++ b/samples/notary-demo/build.gradle @@ -25,9 +25,6 @@ dependencies { compile project(':client:rpc') compile project(':test-utils') compile project(':cordform-common') - - // Javax is required for webapis - compile "org.glassfish.jersey.core:jersey-server:${jersey_version}" } idea { diff --git a/samples/simm-valuation-demo/build.gradle b/samples/simm-valuation-demo/build.gradle index f6c7a28aca..4be015ab67 100644 --- a/samples/simm-valuation-demo/build.gradle +++ b/samples/simm-valuation-demo/build.gradle @@ -35,9 +35,6 @@ dependencies { compile project(':webserver') compile project(':finance') - // Javax is required for webapis - compile "org.glassfish.jersey.core:jersey-server:${jersey_version}" - // Cordapp dependencies // Specify your cordapp's dependencies below, including dependent cordapps compile "com.opengamma.strata:strata-basics:${strata_version}" diff --git a/samples/trader-demo/build.gradle b/samples/trader-demo/build.gradle index e21103cd7a..94dda10602 100644 --- a/samples/trader-demo/build.gradle +++ b/samples/trader-demo/build.gradle @@ -31,23 +31,11 @@ dependencies { compile project(':finance') // Corda Plugins: dependent flows and services - compile project(':samples:bank-of-corda-demo') - - // Javax is required for webapis - compile "org.glassfish.jersey.core:jersey-server:${jersey_version}" - - // GraphStream: For visualisation (required by ExampleClientRPC app) - compile "org.graphstream:gs-core:1.3" - compile("org.graphstream:gs-ui:1.3") { - exclude group: "bouncycastle" - } + cordapp project(':samples:bank-of-corda-demo') testCompile project(':test-utils') testCompile "junit:junit:$junit_version" testCompile "org.assertj:assertj-core:${assertj_version}" - - // Cordapp dependencies - // Specify your cordapp's dependencies below, including dependent cordapps } task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { From 562b186a6539bb0e0bb1fec088576008abb23042 Mon Sep 17 00:00:00 2001 From: Clinton Date: Fri, 30 Jun 2017 16:29:57 +0100 Subject: [PATCH 33/97] Fix IntegrationTestingTutorial network map race condition (#947) * Ignoring and adding a TODO to a test that fails spuriously during CI. * Ensure both parties are regsitered with network map in the integration test tutorial and update docs to reflect this best practice. * Minor readability fix. --- .../kotlin/net/corda/docs/IntegrationTestingTutorial.kt | 3 +++ docs/source/tutorial-integration-testing.rst | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt index 061aaf5524..bd66479c80 100644 --- a/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt +++ b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt @@ -51,6 +51,9 @@ class IntegrationTestingTutorial { val bobClient = bob.rpcClientToNode() val bobProxy = bobClient.start("bobUser", "testPassword2").proxy + + aliceProxy.waitUntilRegisteredWithNetworkMap().getOrThrow() + bobProxy.waitUntilRegisteredWithNetworkMap().getOrThrow() // END 2 // START 3 diff --git a/docs/source/tutorial-integration-testing.rst b/docs/source/tutorial-integration-testing.rst index 3ab804b7b8..0cbed18a15 100644 --- a/docs/source/tutorial-integration-testing.rst +++ b/docs/source/tutorial-integration-testing.rst @@ -35,7 +35,9 @@ notary directly, so there's no need to pass in the test ``User``. The ``startNode`` function returns a future that completes once the node is fully started. This allows starting of the nodes to be parallel. We wait on these futures as we need the information -returned; their respective ``NodeHandles`` s. +returned; their respective ``NodeHandles`` s. After getting the handles we +wait for both parties to register with the network map to ensure we don't +have race conditions with network map registration. .. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt :language: kotlin From fcba32700ceb28e208a2085adf28b08fd4b8b9d9 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Mon, 3 Jul 2017 12:12:28 +0100 Subject: [PATCH 34/97] Review comment changes --- .../serialization/amqp/DeserializationInput.kt | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) 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 bcb168e22d..2859dbb989 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 @@ -11,8 +11,7 @@ import java.lang.reflect.Type import java.nio.ByteBuffer import java.util.* - -data class classAndEnvelope(val obj: T, val envelope: Envelope) +data class objectAndEnvelope(val obj: T, val envelope: Envelope) /** * Main entry point for deserializing an AMQP encoded object. @@ -65,12 +64,12 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory = S @Throws(NotSerializableException::class) - inline fun deserializeAndReturnEnvelope(bytes: SerializedBytes): classAndEnvelope = + inline internal fun deserializeAndReturnEnvelope(bytes: SerializedBytes): objectAndEnvelope = deserializeAndReturnEnvelope(bytes, T::class.java) @Throws(NotSerializableException::class) - fun getEnvelope(bytes: SerializedBytes): Envelope { + private fun getEnvelope(bytes: SerializedBytes): Envelope { // Check that the lead bytes match expected header if (!subArraysEqual(bytes.bytes, 0, 8, AmqpHeaderV1_0.bytes, 0)) { throw NotSerializableException("Serialization header does not match.") @@ -87,7 +86,7 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory = S @Throws(NotSerializableException::class) - fun des(bytes: SerializedBytes, clazz: Class, generator: (SerializedBytes, Class) -> R): R { + private fun des(bytes: SerializedBytes, clazz: Class, generator: (SerializedBytes, Class) -> R): R { try { return generator(bytes, clazz) } catch(nse: NotSerializableException) { @@ -113,11 +112,11 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory = S } @Throws(NotSerializableException::class) - fun deserializeAndReturnEnvelope(bytes: SerializedBytes, clazz: Class): classAndEnvelope { - return des>(bytes, clazz) { bytes, clazz -> + internal fun deserializeAndReturnEnvelope(bytes: SerializedBytes, clazz: Class): objectAndEnvelope { + return des>(bytes, clazz) { bytes, clazz -> val envelope = getEnvelope(bytes) // Now pick out the obj and schema from the envelope. - classAndEnvelope(clazz.cast(readObjectOrNull(envelope.obj, envelope.schema, clazz)), envelope) + objectAndEnvelope(clazz.cast(readObjectOrNull(envelope.obj, envelope.schema, clazz)), envelope) } } From f11f17e2aa71c15f27459962b04545ec049af78c Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Mon, 3 Jul 2017 14:42:59 +0100 Subject: [PATCH 35/97] Move carpenter into core --- .../core/serialization/carpenter}/carpenter/ClassCarpenter.kt | 0 .../net/corda/core/serialization}/carpenter/ClassCarpenterTest.kt | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {experimental/src/main/kotlin/net/corda => core/src/main/kotlin/net/corda/core/serialization/carpenter}/carpenter/ClassCarpenter.kt (100%) rename {experimental/src/test/kotlin/net/corda => core/src/test/kotlin/net/corda/core/serialization}/carpenter/ClassCarpenterTest.kt (100%) diff --git a/experimental/src/main/kotlin/net/corda/carpenter/ClassCarpenter.kt b/core/src/main/kotlin/net/corda/core/serialization/carpenter/carpenter/ClassCarpenter.kt similarity index 100% rename from experimental/src/main/kotlin/net/corda/carpenter/ClassCarpenter.kt rename to core/src/main/kotlin/net/corda/core/serialization/carpenter/carpenter/ClassCarpenter.kt diff --git a/experimental/src/test/kotlin/net/corda/carpenter/ClassCarpenterTest.kt b/core/src/test/kotlin/net/corda/core/serialization/carpenter/ClassCarpenterTest.kt similarity index 100% rename from experimental/src/test/kotlin/net/corda/carpenter/ClassCarpenterTest.kt rename to core/src/test/kotlin/net/corda/core/serialization/carpenter/ClassCarpenterTest.kt From 88ed35636c18f32408ef68adf3a0f29acb6b8af5 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Mon, 3 Jul 2017 15:04:51 +0100 Subject: [PATCH 36/97] Fix mis-move --- .../serialization/carpenter/{carpenter => }/ClassCarpenter.kt | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename core/src/main/kotlin/net/corda/core/serialization/carpenter/{carpenter => }/ClassCarpenter.kt (100%) diff --git a/core/src/main/kotlin/net/corda/core/serialization/carpenter/carpenter/ClassCarpenter.kt b/core/src/main/kotlin/net/corda/core/serialization/carpenter/ClassCarpenter.kt similarity index 100% rename from core/src/main/kotlin/net/corda/core/serialization/carpenter/carpenter/ClassCarpenter.kt rename to core/src/main/kotlin/net/corda/core/serialization/carpenter/ClassCarpenter.kt From 7df8c501675765abf5450ee194a283c207e1ec10 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Mon, 3 Jul 2017 15:53:48 +0100 Subject: [PATCH 37/97] Adds helper methods to grab a LedgerTx or verify a SignedTx. Deprecates builder signing methods. --- .../kotlin/net/corda/core/node/ServiceHub.kt | 2 - .../core/transactions/SignedTransaction.kt | 41 ++++- .../core/transactions/TransactionBuilder.kt | 163 +++++++++--------- .../core/transactions/WireTransaction.kt | 14 ++ .../contracts/TransactionGraphSearchTests.kt | 37 ++-- .../core/flows/CollectSignaturesFlowTests.kt | 6 +- .../core/flows/ResolveTransactionsFlowTest.kt | 36 ++-- .../TransactionSerializationTests.kt | 50 +++--- .../corda/contracts/CommercialPaperTests.kt | 46 ++--- .../net/corda/contracts/asset/CashTests.kt | 25 ++- .../corda/contracts/asset/ObligationTests.kt | 74 ++++---- .../services/vault/NodeVaultServiceTest.kt | 15 +- .../node/services/vault/VaultWithCashTest.kt | 87 +++++----- .../corda/attachmentdemo/AttachmentDemo.kt | 4 +- .../net/corda/irs/flows/RatesFixFlow.kt | 2 +- .../corda/irs/api/NodeInterestRatesTest.kt | 6 +- .../kotlin/net/corda/irs/contract/IRSTests.kt | 22 +-- .../net/corda/traderdemo/flow/SellerFlow.kt | 24 +-- .../kotlin/net/corda/testing/CoreTestUtils.kt | 2 + .../main/kotlin/net/corda/testing/TestDSL.kt | 1 - .../net/corda/loadtest/tests/NotaryTest.kt | 16 +- 21 files changed, 349 insertions(+), 324 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt index dc3ef5d458..09461963fe 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -136,7 +136,6 @@ interface ServiceHub : ServicesForResolution { return builder.toSignedTransaction(false) } - /** * Helper method to construct an initial partially signed transaction from a TransactionBuilder * using the default identity key contained in the node. @@ -146,7 +145,6 @@ interface ServiceHub : ServicesForResolution { */ fun signInitialTransaction(builder: TransactionBuilder): SignedTransaction = signInitialTransaction(builder, legalIdentityKey) - /** * Helper method to construct an initial partially signed transaction from a [TransactionBuilder] * using a set of keys all held in this node. 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 00f83fdf3e..8a4453b2d6 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt @@ -3,9 +3,11 @@ package net.corda.core.transactions import net.corda.core.contracts.AttachmentResolutionException import net.corda.core.contracts.NamedByHash import net.corda.core.contracts.TransactionResolutionException +import net.corda.core.contracts.TransactionVerificationException import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.SecureHash import net.corda.core.crypto.isFulfilledBy +import net.corda.core.crypto.keys import net.corda.core.node.ServiceHub import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializedBytes @@ -136,17 +138,46 @@ data class SignedTransaction(val txBits: SerializedBytes, operator fun plus(sigList: Collection) = withAdditionalSignatures(sigList) /** - * Calls [verifySignatures] to check all required signatures are present, and then calls - * [WireTransaction.toLedgerTransaction] with the passed in [ServiceHub] to resolve the dependencies, - * returning an unverified LedgerTransaction. + * Checks the transaction's signatures are valid, optionally calls [verifySignatures] to check + * all required signatures are present, and then calls [WireTransaction.toLedgerTransaction] + * with the passed in [ServiceHub] to resolve the dependencies, returning an unverified + * LedgerTransaction. + * + * This allows us to perform validation over the entirety of the transaction's contents. + * WireTransaction only contains StateRef for the inputs and hashes for the attachments, + * rather than ContractState instances for the inputs and Attachment instances for the attachments. * * @throws AttachmentResolutionException if a required attachment was not found in storage. * @throws TransactionResolutionException if an input points to a transaction not found in storage. * @throws SignatureException if any signatures were invalid or unrecognised * @throws SignaturesMissingException if any signatures that should have been present are missing. */ - @Throws(AttachmentResolutionException::class, TransactionResolutionException::class, SignatureException::class) - fun toLedgerTransaction(services: ServiceHub) = verifySignatures().toLedgerTransaction(services) + @JvmOverloads + @Throws(SignatureException::class, AttachmentResolutionException::class, TransactionResolutionException::class) + fun toLedgerTransaction(services: ServiceHub, checkSufficientSignatures: Boolean = true): LedgerTransaction { + checkSignaturesAreValid() + if (checkSufficientSignatures) verifySignatures() + return tx.toLedgerTransaction(services) + } + + /** + * Checks the transaction's signatures are valid, optionally calls [verifySignatures] to check + * all required signatures are present, calls [WireTransaction.toLedgerTransaction] with the + * passed in [ServiceHub] to resolve the dependencies and return an unverified + * LedgerTransaction, then verifies the LedgerTransaction. + * + * @throws AttachmentResolutionException if a required attachment was not found in storage. + * @throws TransactionResolutionException if an input points to a transaction not found in storage. + * @throws SignatureException if any signatures were invalid or unrecognised + * @throws SignaturesMissingException if any signatures that should have been present are missing. + */ + @JvmOverloads + @Throws(SignatureException::class, AttachmentResolutionException::class, TransactionResolutionException::class, TransactionVerificationException::class) + fun verify(services: ServiceHub, checkSufficientSignatures: Boolean = true) { + checkSignaturesAreValid() + if (checkSufficientSignatures) verifySignatures() + tx.toLedgerTransaction(services).verify() + } override fun toString(): String = "${javaClass.simpleName}(id=$id)" } 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 e727ad7c7c..abfa261da6 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -1,13 +1,16 @@ package net.corda.core.transactions import co.paralleluniverse.strands.Strand +import com.google.common.annotations.VisibleForTesting import net.corda.core.contracts.* import net.corda.core.crypto.* import net.corda.core.internal.FlowStateMachine import net.corda.core.identity.Party +import net.corda.core.node.ServiceHub import net.corda.core.serialization.serialize import java.security.KeyPair import java.security.PublicKey +import java.security.SignatureException import java.time.Duration import java.time.Instant import java.util.* @@ -40,8 +43,6 @@ open class TransactionBuilder( protected var timeWindow: TimeWindow? = null) { constructor(type: TransactionType, notary: Party) : this(type, notary, (Strand.currentStrand() as? FlowStateMachine<*>)?.id?.uuid ?: UUID.randomUUID()) - 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. */ @@ -57,27 +58,6 @@ open class TransactionBuilder( timeWindow = timeWindow ) - /** - * 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 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 addTimeWindow(time: Instant, timeTolerance: Duration) = addTimeWindow(TimeWindow.withTolerance(time, timeTolerance)) - - fun addTimeWindow(timeWindow: TimeWindow) { - check(notary != null) { "Only notarised transactions can have a time-window" } - signers.add(notary!!.owningKey) - 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 { @@ -95,62 +75,18 @@ open class TransactionBuilder( } // 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 - addSignatureUnchecked(key.sign(data.bytes)) - return this - } - - /** - * Checks that the given signature matches one of the commands and that it is a correct signature over the tx, then - * adds it. - * - * @throws SignatureException if the signature didn't match the transaction contents. - * @throws IllegalArgumentException if the signature key doesn't appear in any command. - */ - fun checkAndAddSignature(sig: DigitalSignature.WithKey) { - checkSignature(sig) - addSignatureUnchecked(sig) - } - - /** - * Checks that the given signature matches one of the commands and that it is a correct signature over the tx. - * - * @throws SignatureException if the signature didn't match the transaction contents. - * @throws IllegalArgumentException if the signature key doesn't appear in any command. - */ - fun checkSignature(sig: DigitalSignature.WithKey) { - require(commands.any { it.signers.any { sig.by in it.keys } }) { "Signature key doesn't match any command" } - sig.verify(toWireTransaction().id) - } - - /** Adds the signature directly to the transaction, without checking it for validity. */ - fun addSignatureUnchecked(sig: DigitalSignature.WithKey): TransactionBuilder { - currentSigs.add(sig) - return this - } - fun toWireTransaction() = WireTransaction(ArrayList(inputs), ArrayList(attachments), ArrayList(outputs), ArrayList(commands), notary, signers.toList(), type, timeWindow) - fun toSignedTransaction(checkSufficientSignatures: Boolean = true): SignedTransaction { - if (checkSufficientSignatures) { - val gotKeys = currentSigs.map { it.by }.toSet() - val missing: Set = signers.filter { !it.isFulfilledBy(gotKeys) }.toSet() - if (missing.isNotEmpty()) - throw IllegalStateException("Missing signatures on the transaction for the public keys: ${missing.joinToString()}") - } - val wtx = toWireTransaction() - return SignedTransaction(wtx.serialize(), ArrayList(currentSigs)) + @Throws(AttachmentResolutionException::class, TransactionResolutionException::class) + fun toLedgerTransaction(services: ServiceHub) = toWireTransaction().toLedgerTransaction(services) + + @Throws(AttachmentResolutionException::class, TransactionResolutionException::class, TransactionVerificationException::class) + fun verify(services: ServiceHub) { + toLedgerTransaction(services).verify() } open fun addInputState(stateAndRef: StateAndRef<*>) { - check(currentSigs.isEmpty()) val notary = stateAndRef.state.notary require(notary == this.notary) { "Input state requires notary \"$notary\" which does not match the transaction notary \"${this.notary}\"." } signers.add(notary.owningKey) @@ -158,12 +94,10 @@ open class TransactionBuilder( } fun addAttachment(attachmentId: SecureHash) { - check(currentSigs.isEmpty()) attachments.add(attachmentId) } fun addOutputState(state: TransactionState<*>): Int { - check(currentSigs.isEmpty()) outputs.add(state) return outputs.size - 1 } @@ -178,7 +112,6 @@ open class TransactionBuilder( } fun addCommand(arg: Command) { - check(currentSigs.isEmpty()) // TODO: replace pubkeys in commands with 'pointers' to keys in signers signers.addAll(arg.signers) commands.add(arg) @@ -187,10 +120,84 @@ open class TransactionBuilder( fun addCommand(data: CommandData, vararg keys: PublicKey) = addCommand(Command(data, listOf(*keys))) fun addCommand(data: CommandData, keys: List) = addCommand(Command(data, keys)) + /** + * 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 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 addTimeWindow(time: Instant, timeTolerance: Duration) = addTimeWindow(TimeWindow.withTolerance(time, timeTolerance)) + + fun addTimeWindow(timeWindow: TimeWindow) { + check(notary != null) { "Only notarised transactions can have a time-window" } + signers.add(notary!!.owningKey) + this.timeWindow = timeWindow + } + // Accessors that yield immutable snapshots. fun inputStates(): List = ArrayList(inputs) - + fun attachments(): List = ArrayList(attachments) fun outputStates(): List> = ArrayList(outputs) fun commands(): List = ArrayList(commands) - fun attachments(): List = ArrayList(attachments) + + /** The signatures that have been collected so far - might be incomplete! */ + @Deprecated("Signatures should be gathered on a SignedTransaction instead.") + protected val currentSigs = arrayListOf() + + @Deprecated("Use ServiceHub.signInitialTransaction() instead.") + fun signWith(key: KeyPair): TransactionBuilder { + val data = toWireTransaction().id + addSignatureUnchecked(key.sign(data.bytes)) + return this + } + + /** Adds the signature directly to the transaction, without checking it for validity. */ + @Deprecated("Use ServiceHub.signInitialTransaction() instead.") + fun addSignatureUnchecked(sig: DigitalSignature.WithKey): TransactionBuilder { + currentSigs.add(sig) + return this + } + + @Deprecated("Use ServiceHub.signInitialTransaction() instead.") + fun toSignedTransaction(checkSufficientSignatures: Boolean = true): SignedTransaction { + if (checkSufficientSignatures) { + val gotKeys = currentSigs.map { it.by }.toSet() + val missing: Set = signers.filter { !it.isFulfilledBy(gotKeys) }.toSet() + if (missing.isNotEmpty()) + throw IllegalStateException("Missing signatures on the transaction for the public keys: ${missing.joinToString()}") + } + val wtx = toWireTransaction() + return SignedTransaction(wtx.serialize(), ArrayList(currentSigs)) + } + + /** + * Checks that the given signature matches one of the commands and that it is a correct signature over the tx, then + * adds it. + * + * @throws SignatureException if the signature didn't match the transaction contents. + * @throws IllegalArgumentException if the signature key doesn't appear in any command. + */ + @Deprecated("Use WireTransaction.checkSignature() instead.") + fun checkAndAddSignature(sig: DigitalSignature.WithKey) { + checkSignature(sig) + addSignatureUnchecked(sig) + } + + /** + * Checks that the given signature matches one of the commands and that it is a correct signature over the tx. + * + * @throws SignatureException if the signature didn't match the transaction contents. + * @throws IllegalArgumentException if the signature key doesn't appear in any command. + */ + @Deprecated("Use WireTransaction.checkSignature() instead.") + fun checkSignature(sig: DigitalSignature.WithKey) { + require(commands.any { it.signers.any { sig.by in it.keys } }) { "Signature key doesn't match any command" } + sig.verify(toWireTransaction().id) + } } 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 18ef251516..684ad86f9b 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt @@ -2,8 +2,10 @@ package net.corda.core.transactions import com.esotericsoftware.kryo.pool.KryoPool import net.corda.core.contracts.* +import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.MerkleTree import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.keys import net.corda.core.identity.Party import net.corda.core.indexOfOrThrow import net.corda.core.node.ServicesForResolution @@ -13,6 +15,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.security.SignatureException import java.util.function.Predicate /** @@ -135,6 +138,17 @@ class WireTransaction( ) } + /** + * Checks that the given signature matches one of the commands and that it is a correct signature over the tx. + * + * @throws SignatureException if the signature didn't match the transaction contents. + * @throws IllegalArgumentException if the signature key doesn't appear in any command. + */ + fun checkSignature(sig: DigitalSignature.WithKey) { + require(commands.any { it.signers.any { sig.by in it.keys } }) { "Signature key doesn't match any command" } + sig.verify(id) + } + override fun toString(): String { val buf = StringBuilder() buf.appendln("Transaction:") diff --git a/core/src/test/kotlin/net/corda/core/contracts/TransactionGraphSearchTests.kt b/core/src/test/kotlin/net/corda/core/contracts/TransactionGraphSearchTests.kt index aee51ffd46..c4557fd75d 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/TransactionGraphSearchTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/TransactionGraphSearchTests.kt @@ -6,6 +6,8 @@ import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.DUMMY_NOTARY import net.corda.core.utilities.DUMMY_NOTARY_KEY import net.corda.testing.MEGA_CORP_KEY +import net.corda.testing.MEGA_CORP_PUBKEY +import net.corda.testing.node.MockServices import net.corda.testing.node.MockTransactionStorage import org.junit.Test import java.security.KeyPair @@ -28,24 +30,29 @@ class TransactionGraphSearchTests { * @param command the command to add to the origin transaction. * @param signer signer for the two transactions and their commands. */ - fun buildTransactions(command: CommandData, signer: KeyPair): GraphTransactionStorage { - val originTx = TransactionType.General.Builder(DUMMY_NOTARY).apply { - addOutputState(DummyState(random31BitValue())) - addCommand(command, signer.public) - signWith(signer) - signWith(DUMMY_NOTARY_KEY) - }.toSignedTransaction(false) - val inputTx = TransactionType.General.Builder(DUMMY_NOTARY).apply { - addInputState(originTx.tx.outRef(0)) - signWith(signer) - signWith(DUMMY_NOTARY_KEY) - }.toSignedTransaction(false) + fun buildTransactions(command: CommandData): GraphTransactionStorage { + val megaCorpServices = MockServices(MEGA_CORP_KEY) + val notaryServices = MockServices(DUMMY_NOTARY_KEY) + + val originBuilder = TransactionType.General.Builder(DUMMY_NOTARY) + originBuilder.addOutputState(DummyState(random31BitValue())) + originBuilder.addCommand(command, MEGA_CORP_PUBKEY) + + val originPtx = megaCorpServices.signInitialTransaction(originBuilder) + val originTx = notaryServices.addSignature(originPtx) + + val inputBuilder = TransactionType.General.Builder(DUMMY_NOTARY) + inputBuilder.addInputState(originTx.tx.outRef(0)) + + val inputPtx = megaCorpServices.signInitialTransaction(inputBuilder) + val inputTx = megaCorpServices.addSignature(inputPtx) + return GraphTransactionStorage(originTx, inputTx) } @Test fun `return empty from empty`() { - val storage = buildTransactions(DummyContract.Commands.Create(), MEGA_CORP_KEY) + val storage = buildTransactions(DummyContract.Commands.Create()) val search = TransactionGraphSearch(storage, emptyList()) search.query = TransactionGraphSearch.Query() val expected = emptyList() @@ -56,7 +63,7 @@ class TransactionGraphSearchTests { @Test fun `return empty from no match`() { - val storage = buildTransactions(DummyContract.Commands.Create(), MEGA_CORP_KEY) + val storage = buildTransactions(DummyContract.Commands.Create()) val search = TransactionGraphSearch(storage, listOf(storage.inputTx.tx)) search.query = TransactionGraphSearch.Query() val expected = emptyList() @@ -67,7 +74,7 @@ class TransactionGraphSearchTests { @Test fun `return origin on match`() { - val storage = buildTransactions(DummyContract.Commands.Create(), MEGA_CORP_KEY) + val storage = buildTransactions(DummyContract.Commands.Create()) val search = TransactionGraphSearch(storage, listOf(storage.inputTx.tx)) search.query = TransactionGraphSearch.Query(DummyContract.Commands.Create::class.java) val expected = listOf(storage.originTx.tx) diff --git a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt index 1396799b8c..91672d3133 100644 --- a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt @@ -13,7 +13,9 @@ import net.corda.flows.CollectSignaturesFlow import net.corda.flows.FinalityFlow import net.corda.flows.SignTransactionFlow import net.corda.testing.MINI_CORP_KEY +import net.corda.testing.MINI_CORP_PUBKEY import net.corda.testing.node.MockNetwork +import net.corda.testing.node.MockServices import org.junit.After import org.junit.Before import org.junit.Test @@ -26,6 +28,7 @@ class CollectSignaturesFlowTests { lateinit var b: MockNetwork.MockNode lateinit var c: MockNetwork.MockNode lateinit var notary: Party + val services = MockServices() @Before fun setup() { @@ -162,7 +165,8 @@ class CollectSignaturesFlowTests { @Test fun `fails when not signed by initiator`() { val onePartyDummyContract = DummyContract.generateInitial(1337, notary, a.info.legalIdentity.ref(1)) - val ptx = onePartyDummyContract.signWith(MINI_CORP_KEY).toSignedTransaction(false) + val miniCorpServices = MockServices(MINI_CORP_KEY) + val ptx = miniCorpServices.signInitialTransaction(onePartyDummyContract) val flow = a.services.startFlow(CollectSignaturesFlow(ptx)) mockNet.runNetwork() assertFailsWith("The Initiator of CollectSignaturesFlow must have signed the transaction.") { diff --git a/core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt index c80ed66f30..8dccc62f9a 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt @@ -14,6 +14,7 @@ import net.corda.testing.MEGA_CORP import net.corda.testing.MEGA_CORP_KEY import net.corda.testing.MINI_CORP import net.corda.testing.node.MockNetwork +import net.corda.testing.node.MockServices import org.junit.After import org.junit.Before import org.junit.Test @@ -32,6 +33,8 @@ class ResolveTransactionsFlowTest { lateinit var a: MockNetwork.MockNode lateinit var b: MockNetwork.MockNode lateinit var notary: Party + val megaCorpServices = MockServices(MEGA_CORP_KEY) + val notaryServices = MockServices(DUMMY_NOTARY_KEY) @Before fun setup() { @@ -94,9 +97,8 @@ class ResolveTransactionsFlowTest { val count = 50 var cursor = stx2 repeat(count) { - val stx = DummyContract.move(cursor.tx.outRef(0), MINI_CORP) - .addSignatureUnchecked(NullSignature) - .toSignedTransaction(false) + val builder = DummyContract.move(cursor.tx.outRef(0), MINI_CORP) + val stx = megaCorpServices.signInitialTransaction(builder) a.database.transaction { a.services.recordTransactions(stx) } @@ -114,15 +116,13 @@ class ResolveTransactionsFlowTest { val stx1 = makeTransactions().first val stx2 = DummyContract.move(stx1.tx.outRef(0), MINI_CORP).run { - signWith(MEGA_CORP_KEY) - signWith(DUMMY_NOTARY_KEY) - toSignedTransaction() + val ptx = megaCorpServices.signInitialTransaction(this) + notaryServices.addSignature(ptx) } val stx3 = DummyContract.move(listOf(stx1.tx.outRef(0), stx2.tx.outRef(0)), MINI_CORP).run { - signWith(MEGA_CORP_KEY) - signWith(DUMMY_NOTARY_KEY) - toSignedTransaction() + val ptx = megaCorpServices.signInitialTransaction(this) + notaryServices.addSignature(ptx) } a.database.transaction { @@ -168,15 +168,19 @@ class ResolveTransactionsFlowTest { val dummy1: SignedTransaction = DummyContract.generateInitial(0, notary, MEGA_CORP.ref(1)).let { if (withAttachment != null) it.addAttachment(withAttachment) - if (signFirstTX) - it.signWith(MEGA_CORP_KEY) - it.signWith(DUMMY_NOTARY_KEY) - it.toSignedTransaction(false) + when (signFirstTX) { + true -> { + val ptx = megaCorpServices.signInitialTransaction(it) + notaryServices.addSignature(ptx) + } + false -> { + notaryServices.signInitialTransaction(it) + } + } } val dummy2: SignedTransaction = DummyContract.move(dummy1.tx.outRef(0), MINI_CORP).let { - it.signWith(MEGA_CORP_KEY) - it.signWith(DUMMY_NOTARY_KEY) - it.toSignedTransaction() + val ptx = megaCorpServices.signInitialTransaction(it) + notaryServices.addSignature(ptx) } a.database.transaction { a.services.recordTransactions(dummy1, dummy2) diff --git a/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt b/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt index eed2941019..5b6217ec58 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt @@ -5,14 +5,9 @@ import net.corda.core.crypto.SecureHash import net.corda.core.identity.AbstractParty import net.corda.core.seconds import net.corda.core.transactions.TransactionBuilder -import net.corda.core.utilities.DUMMY_KEY_2 -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.core.utilities.DUMMY_NOTARY_KEY -import net.corda.core.utilities.TEST_TX_TIME -import net.corda.testing.MEGA_CORP -import net.corda.testing.MEGA_CORP_KEY -import net.corda.testing.MINI_CORP -import net.corda.testing.generateStateRef +import net.corda.core.utilities.* +import net.corda.testing.* +import net.corda.testing.node.MockServices import org.junit.Before import org.junit.Test import java.security.SignatureException @@ -53,7 +48,8 @@ class TransactionSerializationTests { val outputState = TransactionState(TestCash.State(depositRef, 600.POUNDS, MEGA_CORP), DUMMY_NOTARY) val changeState = TransactionState(TestCash.State(depositRef, 400.POUNDS, MEGA_CORP), DUMMY_NOTARY) - + val megaCorpServices = MockServices(MEGA_CORP_KEY) + val notaryServices = MockServices(DUMMY_NOTARY_KEY) lateinit var tx: TransactionBuilder @Before @@ -65,53 +61,47 @@ class TransactionSerializationTests { @Test fun signWireTX() { - tx.signWith(DUMMY_NOTARY_KEY) - tx.signWith(MEGA_CORP_KEY) - val signedTX = tx.toSignedTransaction() + val ptx = megaCorpServices.signInitialTransaction(tx) + val stx = notaryServices.addSignature(ptx) // Now check that the signature we just made verifies. - signedTX.verifySignatures() + stx.verifySignatures() // Corrupt the data and ensure the signature catches the problem. - signedTX.id.bytes[5] = signedTX.id.bytes[5].inc() + stx.id.bytes[5] = stx.id.bytes[5].inc() assertFailsWith(SignatureException::class) { - signedTX.verifySignatures() + stx.verifySignatures() } } @Test fun wrongKeys() { - // Can't convert if we don't have signatures for all commands - assertFailsWith(IllegalStateException::class) { - tx.toSignedTransaction() - } - - tx.signWith(MEGA_CORP_KEY) - tx.signWith(DUMMY_NOTARY_KEY) - val signedTX = tx.toSignedTransaction() + val ptx = megaCorpServices.signInitialTransaction(tx) + val stx = notaryServices.addSignature(ptx) // Cannot construct with an empty sigs list. assertFailsWith(IllegalArgumentException::class) { - signedTX.copy(sigs = emptyList()) + stx.copy(sigs = emptyList()) } // If the signature was replaced in transit, we don't like it. assertFailsWith(SignatureException::class) { val tx2 = TransactionType.General.Builder(DUMMY_NOTARY).withItems(inputState, outputState, changeState, Command(TestCash.Commands.Move(), DUMMY_KEY_2.public)) - tx2.signWith(DUMMY_NOTARY_KEY) - tx2.signWith(DUMMY_KEY_2) - signedTX.copy(sigs = tx2.toSignedTransaction().sigs).verifySignatures() + val ptx2 = notaryServices.signInitialTransaction(tx2) + val dummyServices = MockServices(DUMMY_KEY_2) + val stx2 = dummyServices.addSignature(ptx2) + + stx.copy(sigs = stx2.sigs).verifySignatures() } } @Test fun timeWindow() { tx.addTimeWindow(TEST_TX_TIME, 30.seconds) - tx.signWith(MEGA_CORP_KEY) - tx.signWith(DUMMY_NOTARY_KEY) - val stx = tx.toSignedTransaction() + val ptx = megaCorpServices.signInitialTransaction(tx) + val stx = notaryServices.addSignature(ptx) assertEquals(TEST_TX_TIME, stx.tx.timeWindow?.midpoint) } } diff --git a/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt b/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt index 7dfe0e83c3..caf719047d 100644 --- a/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt @@ -205,6 +205,8 @@ class CommercialPaperTestsGeneric { private lateinit var aliceVaultService: VaultService private lateinit var alicesVault: Vault + private val notaryServices = MockServices(DUMMY_NOTARY_KEY) + private lateinit var moveTX: SignedTransaction @Test @@ -215,7 +217,7 @@ class CommercialPaperTestsGeneric { val databaseAlice = dataSourceAndDatabaseAlice.second databaseAlice.transaction { - aliceServices = object : MockServices() { + aliceServices = object : MockServices(ALICE_KEY) { override val vaultService: VaultService = makeVaultService(dataSourcePropsAlice) override fun recordTransactions(txs: Iterable) { @@ -235,7 +237,7 @@ class CommercialPaperTestsGeneric { val databaseBigCorp = dataSourceAndDatabaseBigCorp.second databaseBigCorp.transaction { - bigCorpServices = object : MockServices() { + bigCorpServices = object : MockServices(BIG_CORP_KEY) { override val vaultService: VaultService = makeVaultService(dataSourcePropsBigCorp) override fun recordTransactions(txs: Iterable) { @@ -257,29 +259,27 @@ class CommercialPaperTestsGeneric { // BigCorp™ issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself. val faceValue = 10000.DOLLARS `issued by` DUMMY_CASH_ISSUER val issuance = bigCorpServices.myInfo.legalIdentity.ref(1) - val issueTX: SignedTransaction = - CommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply { - addTimeWindow(TEST_TX_TIME, 30.seconds) - signWith(bigCorpServices.key) - signWith(DUMMY_NOTARY_KEY) - }.toSignedTransaction() + val issueBuilder = CommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, DUMMY_NOTARY) + issueBuilder.addTimeWindow(TEST_TX_TIME, 30.seconds) + val issuePtx = bigCorpServices.signInitialTransaction(issueBuilder) + val issueTx = notaryServices.addSignature(issuePtx) databaseAlice.transaction { // Alice pays $9000 to BigCorp to own some of their debt. moveTX = run { - val ptx = TransactionType.General.Builder(DUMMY_NOTARY) - aliceVaultService.generateSpend(ptx, 9000.DOLLARS, AnonymousParty(bigCorpServices.key.public)) - CommercialPaper().generateMove(ptx, issueTX.tx.outRef(0), AnonymousParty(aliceServices.key.public)) - ptx.signWith(bigCorpServices.key) - ptx.signWith(aliceServices.key) - ptx.signWith(DUMMY_NOTARY_KEY) - ptx.toSignedTransaction() + val builder = TransactionType.General.Builder(DUMMY_NOTARY) + aliceVaultService.generateSpend(builder, 9000.DOLLARS, AnonymousParty(bigCorpServices.key.public)) + CommercialPaper().generateMove(builder, issueTx.tx.outRef(0), AnonymousParty(aliceServices.key.public)) + val ptx = aliceServices.signInitialTransaction(builder) + val ptx2 = bigCorpServices.addSignature(ptx) + val stx = notaryServices.addSignature(ptx2) + stx } } databaseBigCorp.transaction { // Verify the txns are valid and insert into both sides. - listOf(issueTX, moveTX).forEach { + listOf(issueTx, moveTX).forEach { it.toLedgerTransaction(aliceServices).verify() aliceServices.recordTransactions(it) bigCorpServices.recordTransactions(it) @@ -288,13 +288,13 @@ class CommercialPaperTestsGeneric { databaseBigCorp.transaction { fun makeRedeemTX(time: Instant): Pair { - val ptx = TransactionType.General.Builder(DUMMY_NOTARY) - ptx.addTimeWindow(time, 30.seconds) - CommercialPaper().generateRedeem(ptx, moveTX.tx.outRef(1), bigCorpVaultService) - ptx.signWith(aliceServices.key) - ptx.signWith(bigCorpServices.key) - ptx.signWith(DUMMY_NOTARY_KEY) - return Pair(ptx.toSignedTransaction(), ptx.lockId) + val builder = TransactionType.General.Builder(DUMMY_NOTARY) + builder.addTimeWindow(time, 30.seconds) + CommercialPaper().generateRedeem(builder, moveTX.tx.outRef(1), bigCorpVaultService) + val ptx = aliceServices.signInitialTransaction(builder) + val ptx2 = bigCorpServices.addSignature(ptx) + val stx = notaryServices.addSignature(ptx2) + return Pair(stx, builder.lockId) } val redeemTX = makeRedeemTX(TEST_TX_TIME + 10.days) diff --git a/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt b/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt index 2e822f6a7a..6dff6c60e2 100644 --- a/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt @@ -43,8 +43,8 @@ class CashTests { amount = Amount(amount.quantity, token = amount.token.copy(amount.token.issuer.copy(reference = OpaqueBytes.of(ref)))) ) - lateinit var services: MockServices - val vault: VaultService get() = services.vaultService + lateinit var miniCorpServices: MockServices + val vault: VaultService get() = miniCorpServices.vaultService lateinit var dataSource: Closeable lateinit var database: Database lateinit var vaultStatesUnconsumed: List> @@ -57,7 +57,7 @@ class CashTests { dataSource = dataSourceAndDatabase.first database = dataSourceAndDatabase.second database.transaction { - services = object : MockServices() { + miniCorpServices = object : MockServices(MINI_CORP_KEY) { override val keyManagementService: MockKeyManagementService = MockKeyManagementService(identityService, MINI_CORP_KEY, MEGA_CORP_KEY, OUR_KEY) override val vaultService: VaultService = makeVaultService(dataSourceProps) @@ -70,16 +70,16 @@ class CashTests { } } - services.fillWithSomeTestCash(howMuch = 100.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1, + miniCorpServices.fillWithSomeTestCash(howMuch = 100.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1, issuedBy = MEGA_CORP.ref(1), issuerKey = MEGA_CORP_KEY, ownedBy = OUR_IDENTITY_1) - services.fillWithSomeTestCash(howMuch = 400.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1, + miniCorpServices.fillWithSomeTestCash(howMuch = 400.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1, issuedBy = MEGA_CORP.ref(1), issuerKey = MEGA_CORP_KEY, ownedBy = OUR_IDENTITY_1) - services.fillWithSomeTestCash(howMuch = 80.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1, + miniCorpServices.fillWithSomeTestCash(howMuch = 80.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1, issuedBy = MINI_CORP.ref(1), issuerKey = MINI_CORP_KEY, ownedBy = OUR_IDENTITY_1) - services.fillWithSomeTestCash(howMuch = 80.SWISS_FRANCS, atLeastThisManyStates = 1, atMostThisManyStates = 1, + miniCorpServices.fillWithSomeTestCash(howMuch = 80.SWISS_FRANCS, atLeastThisManyStates = 1, atMostThisManyStates = 1, issuedBy = MINI_CORP.ref(1), issuerKey = MINI_CORP_KEY, ownedBy = OUR_IDENTITY_1) - vaultStatesUnconsumed = services.vaultService.unconsumedStates().toList() + vaultStatesUnconsumed = miniCorpServices.vaultService.unconsumedStates().toList() } } @@ -160,8 +160,7 @@ class CashTests { // Test generation works. val tx: WireTransaction = TransactionType.General.Builder(notary = null).apply { Cash().generateIssue(this, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = AnonymousParty(DUMMY_PUBKEY_1), notary = DUMMY_NOTARY) - signWith(MINI_CORP_KEY) - }.toSignedTransaction().tx + }.toWireTransaction() assertTrue(tx.inputs.isEmpty()) val s = tx.outputs[0].data as Cash.State assertEquals(100.DOLLARS `issued by` MINI_CORP.ref(12, 34), s.amount) @@ -177,8 +176,7 @@ class CashTests { val amount = 100.DOLLARS `issued by` MINI_CORP.ref(12, 34) val tx: WireTransaction = TransactionType.General.Builder(notary = null).apply { Cash().generateIssue(this, amount, owner = AnonymousParty(DUMMY_PUBKEY_1), notary = DUMMY_NOTARY) - signWith(MINI_CORP_KEY) - }.toSignedTransaction().tx + }.toWireTransaction() assertTrue(tx.inputs.isEmpty()) assertEquals(tx.outputs[0], tx.outputs[0]) } @@ -250,8 +248,7 @@ class CashTests { var ptx = TransactionType.General.Builder(DUMMY_NOTARY) Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = MINI_CORP, notary = DUMMY_NOTARY) - ptx.signWith(MINI_CORP_KEY) - val tx = ptx.toSignedTransaction() + val tx = miniCorpServices.signInitialTransaction(ptx) // Include the previously issued cash in a new issuance command ptx = TransactionType.General.Builder(DUMMY_NOTARY) diff --git a/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt b/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt index 58edc73e78..7b9d18161f 100644 --- a/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt @@ -11,6 +11,7 @@ import net.corda.core.identity.AnonymousParty import net.corda.core.serialization.OpaqueBytes import net.corda.core.utilities.* import net.corda.testing.* +import net.corda.testing.node.MockServices import org.junit.Test import java.time.Duration import java.time.temporal.ChronoUnit @@ -39,6 +40,8 @@ class ObligationTests { beneficiary = CHARLIE ) val outState = inState.copy(beneficiary = AnonymousParty(DUMMY_PUBKEY_2)) + val miniCorpServices = MockServices(MINI_CORP_KEY) + val notaryServices = MockServices(DUMMY_NOTARY_KEY) private fun cashObligationTestRoots( group: LedgerDSL @@ -125,8 +128,7 @@ class ObligationTests { val tx = TransactionType.General.Builder(notary = null).apply { Obligation().generateIssue(this, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity, beneficiary = CHARLIE, notary = DUMMY_NOTARY) - signWith(MINI_CORP_KEY) - }.toSignedTransaction().tx + }.toWireTransaction() assertTrue(tx.inputs.isEmpty()) val expected = Obligation.State( obligor = MINI_CORP, @@ -203,12 +205,12 @@ class ObligationTests { val tx = TransactionType.General.Builder(DUMMY_NOTARY).apply { Obligation().generateIssue(this, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity, beneficiary = MINI_CORP, notary = DUMMY_NOTARY) - signWith(MINI_CORP_KEY) - }.toSignedTransaction() + }.toWireTransaction() + // Include the previously issued obligation in a new issuance command val ptx = TransactionType.General.Builder(DUMMY_NOTARY) - ptx.addInputState(tx.tx.outRef>(0)) + ptx.addInputState(tx.outRef>(0)) Obligation().generateIssue(ptx, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity, beneficiary = MINI_CORP, notary = DUMMY_NOTARY) } @@ -220,9 +222,7 @@ class ObligationTests { val obligationBobToAlice = oneMillionDollars.OBLIGATION between Pair(BOB, ALICE) val tx = TransactionType.General.Builder(DUMMY_NOTARY).apply { Obligation().generateCloseOutNetting(this, ALICE, obligationAliceToBob, obligationBobToAlice) - signWith(ALICE_KEY) - signWith(DUMMY_NOTARY_KEY) - }.toSignedTransaction().tx + }.toWireTransaction() assertEquals(0, tx.outputs.size) } @@ -233,9 +233,7 @@ class ObligationTests { val obligationBobToAlice = oneMillionDollars.OBLIGATION between Pair(BOB, ALICE) val tx = TransactionType.General.Builder(DUMMY_NOTARY).apply { Obligation().generateCloseOutNetting(this, ALICE, obligationAliceToBob, obligationBobToAlice) - signWith(ALICE_KEY) - signWith(DUMMY_NOTARY_KEY) - }.toSignedTransaction().tx + }.toWireTransaction() assertEquals(1, tx.outputs.size) val actual = tx.outputs[0].data @@ -249,10 +247,7 @@ class ObligationTests { val obligationBobToAlice = oneMillionDollars.OBLIGATION between Pair(BOB, ALICE) val tx = TransactionType.General.Builder(DUMMY_NOTARY).apply { Obligation().generatePaymentNetting(this, obligationAliceToBob.amount.token, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice) - signWith(ALICE_KEY) - signWith(BOB_KEY) - signWith(DUMMY_NOTARY_KEY) - }.toSignedTransaction().tx + }.toWireTransaction() assertEquals(0, tx.outputs.size) } @@ -263,9 +258,7 @@ class ObligationTests { val obligationBobToAlice = (2000000.DOLLARS `issued by` defaultIssuer).OBLIGATION between Pair(BOB, ALICE) val tx = TransactionType.General.Builder(null).apply { Obligation().generatePaymentNetting(this, obligationAliceToBob.amount.token, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice) - signWith(ALICE_KEY) - signWith(BOB_KEY) - }.toSignedTransaction().tx + }.toWireTransaction() assertEquals(1, tx.outputs.size) val expected = obligationBobToAlice.copy(quantity = obligationBobToAlice.quantity - obligationAliceToBob.quantity) val actual = tx.outputs[0].data @@ -282,30 +275,31 @@ class ObligationTests { var tx = TransactionType.General.Builder(null).apply { Obligation().generateIssue(this, MINI_CORP, megaCorpDollarSettlement.copy(dueBefore = dueBefore), 100.DOLLARS.quantity, beneficiary = MINI_CORP, notary = DUMMY_NOTARY) - signWith(MINI_CORP_KEY) - }.toSignedTransaction() - var stateAndRef = tx.tx.outRef>(0) + } + var stx = miniCorpServices.signInitialTransaction(tx) + var stateAndRef = stx.tx.outRef>(0) // Now generate a transaction marking the obligation as having defaulted tx = TransactionType.General.Builder(DUMMY_NOTARY).apply { Obligation().generateSetLifecycle(this, listOf(stateAndRef), Lifecycle.DEFAULTED, DUMMY_NOTARY) - signWith(MINI_CORP_KEY) - signWith(DUMMY_NOTARY_KEY) - }.toSignedTransaction() - assertEquals(1, tx.tx.outputs.size) - assertEquals(stateAndRef.state.data.copy(lifecycle = Lifecycle.DEFAULTED), tx.tx.outputs[0].data) - tx.verifySignatures() + } + var ptx = miniCorpServices.signInitialTransaction(tx, MINI_CORP_PUBKEY) + stx = notaryServices.addSignature(ptx) + + assertEquals(1, stx.tx.outputs.size) + assertEquals(stateAndRef.state.data.copy(lifecycle = Lifecycle.DEFAULTED), stx.tx.outputs[0].data) + stx.verifySignatures() // And set it back - stateAndRef = tx.tx.outRef>(0) + stateAndRef = stx.tx.outRef>(0) tx = TransactionType.General.Builder(DUMMY_NOTARY).apply { Obligation().generateSetLifecycle(this, listOf(stateAndRef), Lifecycle.NORMAL, DUMMY_NOTARY) - signWith(MINI_CORP_KEY) - signWith(DUMMY_NOTARY_KEY) - }.toSignedTransaction() - assertEquals(1, tx.tx.outputs.size) - assertEquals(stateAndRef.state.data.copy(lifecycle = Lifecycle.NORMAL), tx.tx.outputs[0].data) - tx.verifySignatures() + } + ptx = miniCorpServices.signInitialTransaction(tx) + stx = notaryServices.addSignature(ptx) + assertEquals(1, stx.tx.outputs.size) + assertEquals(stateAndRef.state.data.copy(lifecycle = Lifecycle.NORMAL), stx.tx.outputs[0].data) + stx.verifySignatures() } /** Test generating a transaction to settle an obligation. */ @@ -313,22 +307,18 @@ class ObligationTests { fun `generate settlement transaction`() { val cashTx = TransactionType.General.Builder(null).apply { Cash().generateIssue(this, 100.DOLLARS `issued by` defaultIssuer, MINI_CORP, DUMMY_NOTARY) - signWith(MEGA_CORP_KEY) - }.toSignedTransaction().tx + }.toWireTransaction() // Generate a transaction issuing the obligation val obligationTx = TransactionType.General.Builder(null).apply { Obligation().generateIssue(this, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity, beneficiary = MINI_CORP, notary = DUMMY_NOTARY) - signWith(MINI_CORP_KEY) - }.toSignedTransaction().tx + }.toWireTransaction() // Now generate a transaction settling the obligation val settleTx = TransactionType.General.Builder(DUMMY_NOTARY).apply { Obligation().generateSettle(this, listOf(obligationTx.outRef(0)), listOf(cashTx.outRef(0)), Cash.Commands.Move(), DUMMY_NOTARY) - signWith(DUMMY_NOTARY_KEY) - signWith(MINI_CORP_KEY) - }.toSignedTransaction().tx + }.toWireTransaction() assertEquals(2, settleTx.inputs.size) assertEquals(1, settleTx.outputs.size) } @@ -864,7 +854,7 @@ class ObligationTests { @Test fun `summing balances due between parties`() { val simple: Map, Amount> = mapOf(Pair(Pair(ALICE, BOB), Amount(100000000, GBP))) - val expected: Map = mapOf(Pair(ALICE, -100000000L), Pair(BOB, 100000000L)) + val expected: Map = mapOf(Pair(ALICE, -100000000L), Pair(BOB, 100000000L)) val actual = sumAmountsDue(simple) assertEquals(expected, actual) } diff --git a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt index 7a0ec5171e..ed0ef9e133 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt @@ -394,15 +394,16 @@ class NodeVaultServiceTest { @Test fun addNoteToTransaction() { - database.transaction { + val megaCorpServices = MockServices(MEGA_CORP_KEY) + database.transaction { val freshKey = services.legalIdentityKey // Issue a txn to Send us some Money - val usefulTX = TransactionType.General.Builder(null).apply { + val usefulBuilder = TransactionType.General.Builder(null).apply { Cash().generateIssue(this, 100.DOLLARS `issued by` MEGA_CORP.ref(1), AnonymousParty(freshKey), DUMMY_NOTARY) - signWith(MEGA_CORP_KEY) - }.toSignedTransaction() + } + val usefulTX = megaCorpServices.signInitialTransaction(usefulBuilder) services.recordTransactions(listOf(usefulTX)) @@ -412,10 +413,10 @@ class NodeVaultServiceTest { assertEquals(3, vaultSvc.getTransactionNotes(usefulTX.id).count()) // Issue more Money (GBP) - val anotherTX = TransactionType.General.Builder(null).apply { + val anotherBuilder = TransactionType.General.Builder(null).apply { Cash().generateIssue(this, 200.POUNDS `issued by` MEGA_CORP.ref(1), AnonymousParty(freshKey), DUMMY_NOTARY) - signWith(MEGA_CORP_KEY) - }.toSignedTransaction() + } + val anotherTX = megaCorpServices.signInitialTransaction(anotherBuilder) services.recordTransactions(listOf(anotherTX)) diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt index 5eb53717d2..e857b2f5ca 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt @@ -22,8 +22,7 @@ import net.corda.testing.MEGA_CORP import net.corda.testing.MEGA_CORP_KEY import net.corda.testing.node.MockServices import net.corda.testing.node.makeTestDataSourceProperties -import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.api.Assertions.assertThatThrownBy +import org.assertj.core.api.Assertions.* import org.jetbrains.exposed.sql.Database import org.junit.After import org.junit.Before @@ -42,6 +41,7 @@ class VaultWithCashTest { val vault: VaultService get() = services.vaultService lateinit var dataSource: Closeable lateinit var database: Database + val notaryServices = MockServices(DUMMY_NOTARY_KEY) @Before fun setUp() { @@ -91,32 +91,32 @@ class VaultWithCashTest { @Test fun `issue and spend total correctly and irrelevant ignored`() { + val megaCorpServices = MockServices(MEGA_CORP_KEY) + database.transaction { // A tx that sends us money. val freshKey = services.keyManagementService.freshKey() - val usefulTX = TransactionType.General.Builder(null).apply { - Cash().generateIssue(this, 100.DOLLARS `issued by` MEGA_CORP.ref(1), AnonymousParty(freshKey), DUMMY_NOTARY) - signWith(MEGA_CORP_KEY) - }.toSignedTransaction() + val usefulBuilder = TransactionType.General.Builder(null) + Cash().generateIssue(usefulBuilder, 100.DOLLARS `issued by` MEGA_CORP.ref(1), AnonymousParty(freshKey), DUMMY_NOTARY) + val usefulTX = megaCorpServices.signInitialTransaction(usefulBuilder) assertNull(vault.cashBalances[USD]) services.recordTransactions(usefulTX) // A tx that spends our money. - val spendTXBuilder = TransactionType.General.Builder(DUMMY_NOTARY).apply { - vault.generateSpend(this, 80.DOLLARS, BOB) - signWith(DUMMY_NOTARY_KEY) - } - val spendTX = services.signInitialTransaction(spendTXBuilder, freshKey) + val spendTXBuilder = TransactionType.General.Builder(DUMMY_NOTARY) + vault.generateSpend(spendTXBuilder, 80.DOLLARS, BOB) + val spendPTX = services.signInitialTransaction(spendTXBuilder, freshKey) + val spendTX = notaryServices.addSignature(spendPTX) assertEquals(100.DOLLARS, vault.cashBalances[USD]) // A tx that doesn't send us anything. - val irrelevantTX = TransactionType.General.Builder(DUMMY_NOTARY).apply { - Cash().generateIssue(this, 100.DOLLARS `issued by` MEGA_CORP.ref(1), BOB, DUMMY_NOTARY) - signWith(MEGA_CORP_KEY) - signWith(DUMMY_NOTARY_KEY) - }.toSignedTransaction() + val irrelevantBuilder = TransactionType.General.Builder(DUMMY_NOTARY) + Cash().generateIssue(irrelevantBuilder, 100.DOLLARS `issued by` MEGA_CORP.ref(1), BOB, DUMMY_NOTARY) + + val irrelevantPTX = megaCorpServices.signInitialTransaction(irrelevantBuilder) + val irrelevantTX = notaryServices.addSignature(irrelevantPTX) services.recordTransactions(irrelevantTX) assertEquals(100.DOLLARS, vault.cashBalances[USD]) @@ -150,12 +150,10 @@ class VaultWithCashTest { backgroundExecutor.submit { database.transaction { try { - val txn1Builder = - TransactionType.General.Builder(DUMMY_NOTARY).apply { - vault.generateSpend(this, 60.DOLLARS, BOB) - signWith(DUMMY_NOTARY_KEY) - } - val txn1 = services.signInitialTransaction(txn1Builder, freshKey) + val txn1Builder = TransactionType.General.Builder(DUMMY_NOTARY) + vault.generateSpend(txn1Builder, 60.DOLLARS, BOB) + val ptxn1 = notaryServices.signInitialTransaction(txn1Builder) + val txn1 = services.addSignature(ptxn1, freshKey) println("txn1: ${txn1.id} spent ${((txn1.tx.outputs[0].data) as Cash.State).amount}") println("""txn1 states: UNCONSUMED: ${vault.unconsumedStates().count()} : ${vault.unconsumedStates()}, @@ -182,12 +180,10 @@ class VaultWithCashTest { backgroundExecutor.submit { database.transaction { try { - val txn2Builder = - TransactionType.General.Builder(DUMMY_NOTARY).apply { - vault.generateSpend(this, 80.DOLLARS, BOB) - signWith(DUMMY_NOTARY_KEY) - } - val txn2 = services.signInitialTransaction(txn2Builder, freshKey) + val txn2Builder = TransactionType.General.Builder(DUMMY_NOTARY) + vault.generateSpend(txn2Builder, 80.DOLLARS, BOB) + val ptxn2 = notaryServices.signInitialTransaction(txn2Builder) + val txn2 = services.addSignature(ptxn2, freshKey) println("txn2: ${txn2.id} spent ${((txn2.tx.outputs[0].data) as Cash.State).amount}") println("""txn2 states: UNCONSUMED: ${vault.unconsumedStates().count()} : ${vault.unconsumedStates()}, @@ -229,9 +225,8 @@ class VaultWithCashTest { val dummyIssueBuilder = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshIdentity))) addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshIdentity))) - signWith(DUMMY_NOTARY_KEY) } - val dummyIssue = services.signInitialTransaction(dummyIssueBuilder) + val dummyIssue = notaryServices.signInitialTransaction(dummyIssueBuilder) assertThatThrownBy { dummyIssue.toLedgerTransaction(services).verify() @@ -248,11 +243,10 @@ class VaultWithCashTest { val linearId = UniqueIdentifier() // Issue a linear state - val dummyIssueBuilder = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { - addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshIdentity))) - signWith(DUMMY_NOTARY_KEY) - } - val dummyIssue = services.signInitialTransaction(dummyIssueBuilder, services.legalIdentityKey) + val dummyIssueBuilder = TransactionType.General.Builder(notary = DUMMY_NOTARY) + dummyIssueBuilder.addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshIdentity))) + val dummyIssuePtx = notaryServices.signInitialTransaction(dummyIssueBuilder) + val dummyIssue = services.addSignature(dummyIssuePtx) dummyIssue.toLedgerTransaction(services).verify() @@ -260,11 +254,12 @@ class VaultWithCashTest { assertThat(vault.unconsumedStates()).hasSize(1) // Move the same state - val dummyMove = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { + val dummyMoveBuilder = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshIdentity))) addInputState(dummyIssue.tx.outRef(0)) - signWith(DUMMY_NOTARY_KEY) - }.toSignedTransaction() + } + + val dummyMove = notaryServices.signInitialTransaction(dummyMoveBuilder) dummyIssue.toLedgerTransaction(services).verify() @@ -291,11 +286,10 @@ class VaultWithCashTest { database.transaction { // A tx that spends our money. - val spendTXBuilder = TransactionType.General.Builder(DUMMY_NOTARY).apply { - vault.generateSpend(this, 80.DOLLARS, BOB) - signWith(DUMMY_NOTARY_KEY) - } - val spendTX = services.signInitialTransaction(spendTXBuilder, freshKey) + val spendTXBuilder = TransactionType.General.Builder(DUMMY_NOTARY) + vault.generateSpend(spendTXBuilder, 80.DOLLARS, BOB) + val spendPTX = notaryServices.signInitialTransaction(spendTXBuilder) + val spendTX = services.addSignature(spendPTX, freshKey) services.recordTransactions(spendTX) val consumedStates = vault.consumedStates() @@ -322,13 +316,14 @@ class VaultWithCashTest { linearStates.forEach { println(it.state.data.linearId) } // Create a txn consuming different contract types - val dummyMove = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { + val dummyMoveBuilder = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { addOutputState(DummyLinearContract.State(participants = listOf(freshIdentity))) addOutputState(DummyDealContract.State(ref = "999", participants = listOf(freshIdentity))) addInputState(linearStates.first()) addInputState(deals.first()) - signWith(DUMMY_NOTARY_KEY) - }.toSignedTransaction() + } + + val dummyMove = notaryServices.signInitialTransaction(dummyMoveBuilder) dummyMove.toLedgerTransaction(services).verify() services.recordTransactions(dummyMove) diff --git a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt index 739bebc8f8..82a7ed46b2 100644 --- a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt +++ b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt @@ -108,11 +108,9 @@ class AttachmentDemoFlow(val otherSide: Party, val hash: SecureHash.SHA256) : Fl ptx.addAttachment(hash) progressTracker.currentStep = SIGNING - // Sign with the notary key - ptx.signWith(DUMMY_NOTARY_KEY) // Send the transaction to the other recipient - val stx = ptx.toSignedTransaction() + val stx = serviceHub.signInitialTransaction(ptx) return subFlow(FinalityFlow(stx, setOf(otherSide))).single() } diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt index 61f4256ce4..2d5b7b0986 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt @@ -117,7 +117,7 @@ open class RatesFixFlow(protected val tx: TransactionBuilder, val resp = sendAndReceive(oracle, SignRequest(partialMerkleTx)) return resp.unwrap { sig -> check(sig.signer == oracle) - tx.checkSignature(sig) + tx.toWireTransaction().checkSignature(sig) sig } } diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt index 9f2eb05981..9d23175c3f 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt @@ -164,7 +164,7 @@ class NodeInterestRatesTest { val wtx = tx.toWireTransaction() val ftx = wtx.buildFilteredTransaction(Predicate { x -> fixCmdFilter(x) }) val signature = oracle.sign(ftx) - tx.checkAndAddSignature(signature) + wtx.checkSignature(signature) } } @@ -228,8 +228,8 @@ class NodeInterestRatesTest { val future = n1.services.startFlow(flow).resultFuture mockNet.runNetwork() future.getOrThrow() - // We should now have a valid signature over our tx from the oracle. - val fix = tx.toSignedTransaction(true).tx.commands.map { it.value as Fix }.first() + // We should now have a valid fix of our tx from the oracle. + val fix = tx.toWireTransaction().commands.map { it.value as Fix }.first() assertEquals(fixOf, fix.of) assertEquals("0.678".bd, fix.value) } diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/contract/IRSTests.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/contract/IRSTests.kt index 78b2e7974e..2845d4f146 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/contract/IRSTests.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/contract/IRSTests.kt @@ -201,6 +201,10 @@ fun createDummyIRS(irsSelect: Int): InterestRateSwap.State { } class IRSTests { + val megaCorpServices = MockServices(MEGA_CORP_KEY) + val miniCorpServices = MockServices(MINI_CORP_KEY) + val notaryServices = MockServices(DUMMY_NOTARY_KEY) + @Test fun ok() { trade().verifies() @@ -224,11 +228,10 @@ class IRSTests { common = dummyIRS.common, notary = DUMMY_NOTARY).apply { addTimeWindow(TEST_TX_TIME, 30.seconds) - signWith(MEGA_CORP_KEY) - signWith(MINI_CORP_KEY) - signWith(DUMMY_NOTARY_KEY) } - gtx.toSignedTransaction() + val ptx1 = megaCorpServices.signInitialTransaction(gtx) + val ptx2 = miniCorpServices.addSignature(ptx1) + notaryServices.addSignature(ptx2) } return genTX } @@ -308,13 +311,10 @@ class IRSTests { val tx = TransactionType.General.Builder(DUMMY_NOTARY) val fixing = Fix(nextFix, "0.052".percent.value) InterestRateSwap().generateFix(tx, previousTXN.tx.outRef(0), fixing) - with(tx) { - addTimeWindow(TEST_TX_TIME, 30.seconds) - signWith(MEGA_CORP_KEY) - signWith(MINI_CORP_KEY) - signWith(DUMMY_NOTARY_KEY) - } - tx.toSignedTransaction() + tx.addTimeWindow(TEST_TX_TIME, 30.seconds) + val ptx1 = megaCorpServices.signInitialTransaction(tx) + val ptx2 = miniCorpServices.addSignature(ptx1) + notaryServices.addSignature(ptx2) } fixTX.toLedgerTransaction(services).verify() services.recordTransactions(fixTX) diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt index 7d0a633e70..6a096caf0d 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt @@ -16,6 +16,7 @@ import net.corda.core.node.NodeInfo import net.corda.core.seconds import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.ProgressTracker +import net.corda.flows.FinalityFlow import net.corda.flows.NotaryFlow import net.corda.flows.TwoPartyTradeFlow import net.corda.testing.BOC @@ -69,8 +70,7 @@ class SellerFlow(val otherParty: Party, @Suspendable fun selfIssueSomeCommercialPaper(ownedBy: AbstractParty, notaryNode: NodeInfo): StateAndRef { // Make a fake company that's issued its own paper. - val keyPair = generateKeyPair() - val party = Party(BOC.name, keyPair.public) + val party = Party(BOC.name, serviceHub.legalIdentityKey) val issuance: SignedTransaction = run { val tx = CommercialPaper().generateIssue(party.ref(1, 2, 3), 1100.DOLLARS `issued by` DUMMY_CASH_ISSUER, @@ -85,29 +85,17 @@ class SellerFlow(val otherParty: Party, tx.addTimeWindow(Instant.now(), 30.seconds) // Sign it as ourselves. - tx.signWith(keyPair) + val stx = serviceHub.signInitialTransaction(tx) - // Get the notary to sign the time-window. - val notarySigs = subFlow(NotaryFlow.Client(tx.toSignedTransaction(false))) - notarySigs.forEach { tx.addSignatureUnchecked(it) } - - // Commit it to local storage. - val stx = tx.toSignedTransaction(true) - serviceHub.recordTransactions(listOf(stx)) - - stx + subFlow(FinalityFlow(stx)).single() } // Now make a dummy transaction that moves it to a new key, just to show that resolving dependencies works. val move: SignedTransaction = run { val builder = TransactionType.General.Builder(notaryNode.notaryIdentity) CommercialPaper().generateMove(builder, issuance.tx.outRef(0), ownedBy) - builder.signWith(keyPair) - val notarySignature = subFlow(NotaryFlow.Client(builder.toSignedTransaction(false))) - notarySignature.forEach { builder.addSignatureUnchecked(it) } - val tx = builder.toSignedTransaction(true) - serviceHub.recordTransactions(listOf(tx)) - tx + val stx = serviceHub.signInitialTransaction(builder) + subFlow(FinalityFlow(stx)).single() } return move.tx.outRef(0) diff --git a/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt b/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt index 6687956eee..c550aae6d3 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt @@ -53,6 +53,8 @@ import java.util.concurrent.atomic.AtomicInteger * - The Int.DOLLARS syntax doesn't work from Java. Use the DOLLARS(int) function instead. */ +// TODO: Refactor these dummies to work with the new identities framework. + // A few dummy values for testing. val MEGA_CORP_KEY: KeyPair by lazy { generateKeyPair() } val MEGA_CORP_PUBKEY: PublicKey get() = MEGA_CORP_KEY.public diff --git a/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt b/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt index 4eb3669952..b132d6516f 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt @@ -139,7 +139,6 @@ data class TestTransactionDSLInterpreter private constructor( // Verify on a copy of the transaction builder, so if it's then further modified it doesn't error due to // the existing signature transactionBuilder.copy().apply { - signWith(DUMMY_NOTARY_KEY) toWireTransaction().toLedgerTransaction(services).verify() } return EnforceVerifyOrFail.Token diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt index 60c45a1cbf..00c1773c6b 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt @@ -14,6 +14,7 @@ import net.corda.core.transactions.SignedTransaction import net.corda.flows.FinalityFlow import net.corda.loadtest.LoadTest import net.corda.loadtest.NodeConnection +import net.corda.testing.node.MockServices import org.slf4j.LoggerFactory private val log = LoggerFactory.getLogger("NotaryTest") @@ -23,16 +24,15 @@ data class NotariseCommand(val issueTx: SignedTransaction, val moveTx: SignedTra val dummyNotarisationTest = LoadTest( "Notarising dummy transactions", generate = { _, _ -> + val issuerServices = MockServices(DUMMY_CASH_ISSUER_KEY) val generateTx = Generator.pickOne(simpleNodes).bind { node -> Generator.int().map { - val issueTx = DummyContract.generateInitial(it, notary.info.notaryIdentity, DUMMY_CASH_ISSUER).apply { - signWith(DUMMY_CASH_ISSUER_KEY) - } - val asset = issueTx.toWireTransaction().outRef(0) - val moveTx = DummyContract.move(asset, DUMMY_CASH_ISSUER.party).apply { - signWith(DUMMY_CASH_ISSUER_KEY) - } - NotariseCommand(issueTx.toSignedTransaction(false), moveTx.toSignedTransaction(false), node) + val issueBuilder = DummyContract.generateInitial(it, notary.info.notaryIdentity, DUMMY_CASH_ISSUER) + val issueTx = issuerServices.signInitialTransaction(issueBuilder) + val asset = issueTx.tx.outRef(0) + val moveBuilder = DummyContract.move(asset, DUMMY_CASH_ISSUER.party) + val moveTx = issuerServices.signInitialTransaction(moveBuilder) + NotariseCommand(issueTx, moveTx, node) } } Generator.replicate(10, generateTx) From 46e23b77160bfdf1d49b5151f26f4e3cecbbf039 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Mon, 3 Jul 2017 14:29:18 +0100 Subject: [PATCH 38/97] Clean up of ServiceHubInternal, including how it's created in AbstractNode --- .../core/contracts/TransactionGraphSearch.kt | 4 +- .../kotlin/net/corda/core/node/ServiceHub.kt | 85 ++++---- .../core/node/services/AttachmentStorage.kt | 18 +- .../core/node/services/TransactionStorage.kt | 18 +- .../kotlin/net/corda/flows/FinalityFlow.kt | 10 +- .../core/flows/ContractUpgradeFlowTest.kt | 4 +- .../net/corda/flows/CashPaymentFlowTests.kt | 4 +- .../node/services/RaftNotaryServiceTests.kt | 24 +-- .../net/corda/node/internal/AbstractNode.kt | 201 ++++++++---------- .../node/services/api/ServiceHubInternal.kt | 19 +- .../services/events/NodeSchedulerService.kt | 4 +- .../persistence/DBTransactionStorage.kt | 4 +- .../persistence/NodeAttachmentService.kt | 19 +- .../node/messaging/TwoPartyTradeFlowTests.kt | 14 +- .../node/services/MockServiceHubInternal.kt | 7 +- .../corda/node/services/NotaryChangeTests.kt | 10 +- .../events/NodeSchedulerServiceTest.kt | 4 +- .../network/InMemoryNetworkMapCacheTest.kt | 6 +- .../transactions/NotaryServiceTests.kt | 2 +- .../ValidatingNotaryServiceTests.kt | 2 +- .../services/vault/NodeVaultServiceTest.kt | 4 +- .../net/corda/netmap/simulation/Simulation.kt | 2 +- .../net/corda/traderdemo/flow/BuyerFlow.kt | 12 +- .../kotlin/net/corda/testing/node/MockNode.kt | 9 +- .../net/corda/testing/node/MockServices.kt | 11 +- 25 files changed, 221 insertions(+), 276 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/contracts/TransactionGraphSearch.kt b/core/src/main/kotlin/net/corda/core/contracts/TransactionGraphSearch.kt index 4e9bc3e006..4239b5772b 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TransactionGraphSearch.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TransactionGraphSearch.kt @@ -1,7 +1,7 @@ package net.corda.core.contracts import net.corda.core.crypto.SecureHash -import net.corda.core.node.services.ReadOnlyTransactionStorage +import net.corda.core.node.services.TransactionStorage import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction import java.util.* @@ -18,7 +18,7 @@ import java.util.concurrent.Callable * @param transactions map of transaction id to [SignedTransaction]. * @param startPoints transactions to use as starting points for the search. */ -class TransactionGraphSearch(val transactions: ReadOnlyTransactionStorage, +class TransactionGraphSearch(val transactions: TransactionStorage, val startPoints: List) : Callable> { class Query( val withCommandOfType: Class? = null, diff --git a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt index 09461963fe..081aea4d7f 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -1,5 +1,6 @@ package net.corda.core.node +import com.google.common.collect.Lists import net.corda.core.contracts.* import net.corda.core.crypto.DigitalSignature import net.corda.core.node.services.* @@ -17,8 +18,10 @@ import java.time.Clock */ interface ServicesForResolution { val identityService: IdentityService + /** Provides access to storage of arbitrary JAR files (which may contain only data, no code). */ val attachments: AttachmentStorage + /** * Given a [StateRef] loads the referenced transaction and looks up the specified output [ContractState]. * @@ -40,12 +43,13 @@ interface ServiceHub : ServicesForResolution { val vaultService: VaultService val vaultQueryService: VaultQueryService val keyManagementService: KeyManagementService + /** * A map of hash->tx where tx has been signature/contract validated and the states are known to be correct. * The signatures aren't technically needed after that point, but we keep them around so that we can relay * the transaction data to other nodes that need it. */ - val validatedTransactions: ReadOnlyTransactionStorage + val validatedTransactions: TransactionStorage val networkMapCache: NetworkMapCache val transactionVerifierService: TransactionVerifierService @@ -60,41 +64,41 @@ interface ServiceHub : ServicesForResolution { fun cordaService(type: Class): T /** - * Given a [SignedTransaction], writes it to the local storage for validated transactions and then - * sends them to the vault for further processing. Expects to be run within a database transaction. + * Stores the given [SignedTransaction]s in the local transaction storage and then sends them to the vault for + * further processing. This is expected to be run within a database transaction. * * @param txs The transactions to record. */ - // TODO: Make this take a single tx. fun recordTransactions(txs: Iterable) /** - * Given some [SignedTransaction]s, writes them to the local storage for validated transactions and then - * sends them to the vault for further processing. - * - * @param txs The transactions to record. + * Stores the given [SignedTransaction]s in the local transaction storage and then sends them to the vault for + * further processing. This is expected to be run within a database transaction. */ - fun recordTransactions(vararg txs: SignedTransaction) = recordTransactions(txs.toList()) + fun recordTransactions(first: SignedTransaction, vararg remaining: SignedTransaction) { + recordTransactions(Lists.asList(first, remaining)) + } /** * Given a [StateRef] loads the referenced transaction and looks up the specified output [ContractState]. * - * @throws TransactionResolutionException if the [StateRef] points to a non-existent transaction. + * @throws TransactionResolutionException if [stateRef] points to a non-existent transaction. */ @Throws(TransactionResolutionException::class) override fun loadState(stateRef: StateRef): TransactionState<*> { - val definingTx = validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash) - return definingTx.tx.outputs[stateRef.index] + val stx = validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash) + return stx.tx.outputs[stateRef.index] } /** - * Will check [logicType] and [args] against a whitelist and if acceptable then construct and initiate the protocol. + * Converts the given [StateRef] into a [StateAndRef] object. * - * @throws IllegalProtocolLogicException or IllegalArgumentException if there are problems with the [logicType] or [args]. + * @throws TransactionResolutionException if [stateRef] points to a non-existent transaction. */ - fun toStateAndRef(ref: StateRef): StateAndRef { - val definingTx = validatedTransactions.getTransaction(ref.txhash) ?: throw TransactionResolutionException(ref.txhash) - return definingTx.tx.outRef(ref.index) + @Throws(TransactionResolutionException::class) + fun toStateAndRef(stateRef: StateRef): StateAndRef { + val stx = validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash) + return stx.tx.outRef(stateRef.index) } /** @@ -102,7 +106,7 @@ interface ServiceHub : ServicesForResolution { * Node's primary signing identity. * Typical use is during signing in flows and for unit test signing. * When this [PublicKey] is passed into the signing methods below, or on the KeyManagementService - * the matching [PrivateKey] will be looked up internally and used to sign. + * the matching [java.security.PrivateKey] will be looked up internally and used to sign. * If the key is actually a CompositeKey, the first leaf key hosted on this node * will be used to create the signature. */ @@ -114,8 +118,8 @@ interface ServiceHub : ServicesForResolution { * otherwise an IllegalArgumentException will be thrown. * Typical use is during signing in flows and for unit test signing. * When this [PublicKey] is passed into the signing methods below, or on the KeyManagementService - * the matching [PrivateKey] will be looked up internally and used to sign. - * If the key is actually a [CompositeKey], the first leaf key hosted on this node + * the matching [java.security.PrivateKey] will be looked up internally and used to sign. + * If the key is actually a [net.corda.core.crypto.CompositeKey], the first leaf key hosted on this node * will be used to create the signature. */ val notaryIdentityKey: PublicKey get() = this.myInfo.notaryIdentity.owningKey @@ -125,7 +129,7 @@ interface ServiceHub : ServicesForResolution { * using keys stored inside the node. * @param builder The [TransactionBuilder] to seal with the node's signature. * Any existing signatures on the builder will be preserved. - * @param publicKey The [PublicKey] matched to the internal [PrivateKey] to use in signing this transaction. + * @param publicKey The [PublicKey] matched to the internal [java.security.PrivateKey] to use in signing this transaction. * If the passed in key is actually a CompositeKey the code searches for the first child key hosted within this node * to sign with. * @return Returns a SignedTransaction with the new node signature attached. @@ -150,30 +154,30 @@ interface ServiceHub : ServicesForResolution { * using a set of keys all held in this node. * @param builder The [TransactionBuilder] to seal with the node's signature. * Any existing signatures on the builder will be preserved. - * @param signingPubKeys A list of [PublicKeys] used to lookup the matching [PrivateKey] and sign. + * @param signingPubKeys A list of [PublicKey]s used to lookup the matching [java.security.PrivateKey] and sign. * @throws IllegalArgumentException is thrown if any keys are unavailable locally. * @return Returns a [SignedTransaction] with the new node signature attached. */ fun signInitialTransaction(builder: TransactionBuilder, signingPubKeys: Iterable): SignedTransaction { - var stx: SignedTransaction? = null - for (pubKey in signingPubKeys) { - stx = if (stx == null) { - signInitialTransaction(builder, pubKey) - } else { - addSignature(stx, pubKey) - } + val it = signingPubKeys.iterator() + var stx = signInitialTransaction(builder, it.next()) + while (it.hasNext()) { + stx = addSignature(stx, it.next()) } - return stx!! + return stx } /** * Helper method to create an additional signature for an existing (partially) [SignedTransaction]. * @param signedTransaction The [SignedTransaction] to which the signature will apply. - * @param publicKey The [PublicKey] matching to a signing [PrivateKey] hosted in the node. - * If the [PublicKey] is actually a [CompositeKey] the first leaf key found locally will be used for signing. - * @return The [DigitalSignature.WithKey] generated by signing with the internally held [PrivateKey]. + * @param publicKey The [PublicKey] matching to a signing [java.security.PrivateKey] hosted in the node. + * If the [PublicKey] is actually a [net.corda.core.crypto.CompositeKey] the first leaf key found locally will be used + * for signing. + * @return The [DigitalSignature.WithKey] generated by signing with the internally held [java.security.PrivateKey]. */ - fun createSignature(signedTransaction: SignedTransaction, publicKey: PublicKey): DigitalSignature.WithKey = keyManagementService.sign(signedTransaction.id.bytes, publicKey) + fun createSignature(signedTransaction: SignedTransaction, publicKey: PublicKey): DigitalSignature.WithKey { + return keyManagementService.sign(signedTransaction.id.bytes, publicKey) + } /** * Helper method to create an additional signature for an existing (partially) SignedTransaction @@ -181,16 +185,21 @@ interface ServiceHub : ServicesForResolution { * @param signedTransaction The SignedTransaction to which the signature will apply. * @return The DigitalSignature.WithKey generated by signing with the internally held identity PrivateKey. */ - fun createSignature(signedTransaction: SignedTransaction): DigitalSignature.WithKey = createSignature(signedTransaction, legalIdentityKey) + fun createSignature(signedTransaction: SignedTransaction): DigitalSignature.WithKey { + return createSignature(signedTransaction, legalIdentityKey) + } /** * Helper method to append an additional signature to an existing (partially) [SignedTransaction]. * @param signedTransaction The [SignedTransaction] to which the signature will be added. - * @param publicKey The [PublicKey] matching to a signing [PrivateKey] hosted in the node. - * If the [PublicKey] is actually a [CompositeKey] the first leaf key found locally will be used for signing. + * @param publicKey The [PublicKey] matching to a signing [java.security.PrivateKey] hosted in the node. + * If the [PublicKey] is actually a [net.corda.core.crypto.CompositeKey] the first leaf key found locally will be used + * for signing. * @return A new [SignedTransaction] with the addition of the new signature. */ - fun addSignature(signedTransaction: SignedTransaction, publicKey: PublicKey): SignedTransaction = signedTransaction + createSignature(signedTransaction, publicKey) + fun addSignature(signedTransaction: SignedTransaction, publicKey: PublicKey): SignedTransaction { + return signedTransaction + createSignature(signedTransaction, publicKey) + } /** * Helper method to ap-pend an additional signature for an existing (partially) [SignedTransaction] diff --git a/core/src/main/kotlin/net/corda/core/node/services/AttachmentStorage.kt b/core/src/main/kotlin/net/corda/core/node/services/AttachmentStorage.kt index 93ce39069d..af542a2b43 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/AttachmentStorage.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/AttachmentStorage.kt @@ -2,21 +2,14 @@ package net.corda.core.node.services import net.corda.core.contracts.Attachment import net.corda.core.crypto.SecureHash +import java.io.IOException import java.io.InputStream -import java.nio.file.Path +import java.nio.file.FileAlreadyExistsException /** * An attachment store records potentially large binary objects, identified by their hash. */ interface AttachmentStorage { - /** - * If true, newly inserted attachments will be unzipped to a subdirectory of the [storePath]. This is intended for - * human browsing convenience: the attachment itself will still be the file (that is, edits to the extracted directory - * will not have any effect). - */ - var automaticallyExtractAttachments: Boolean - var storePath: Path - /** * Returns a handle to a locally stored attachment, or null if it's not known. The handle can be used to open * a stream for the data, which will be a zip/jar file. @@ -27,13 +20,14 @@ interface AttachmentStorage { * Inserts the given attachment into the store, does *not* close the input stream. This can be an intensive * operation due to the need to copy the bytes to disk and hash them along the way. * - * Note that you should not pass a [JarInputStream] into this method and it will throw if you do, because access - * to the raw byte stream is required. + * Note that you should not pass a [java.util.jar.JarInputStream] into this method and it will throw if you do, because + * access to the raw byte stream is required. * * @throws FileAlreadyExistsException if the given byte stream has already been inserted. - * @throws IllegalArgumentException if the given byte stream is empty or a [JarInputStream]. + * @throws IllegalArgumentException if the given byte stream is empty or a [java.util.jar.JarInputStream]. * @throws IOException if something went wrong. */ + @Throws(FileAlreadyExistsException::class, IOException::class) fun importAttachment(jar: InputStream): SecureHash } diff --git a/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt b/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt index 380ba12365..8173616eda 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt @@ -8,7 +8,7 @@ import rx.Observable /** * Thread-safe storage of transactions. */ -interface ReadOnlyTransactionStorage { +interface TransactionStorage { /** * Return the transaction with the given [id], or null if no such transaction exists. */ @@ -24,18 +24,4 @@ interface ReadOnlyTransactionStorage { * Returns all currently stored transactions and further fresh ones. */ fun track(): DataFeed, SignedTransaction> -} - -/** - * Thread-safe storage of transactions. - */ -interface TransactionStorage : ReadOnlyTransactionStorage { - /** - * Add a new transaction to the store. If the store already has a transaction with the same id it will be - * overwritten. - * @param transaction The transaction to be recorded. - * @return true if the transaction was recorded successfully, false if it was already recorded. - */ - // TODO: Throw an exception if trying to add a transaction with fewer signatures than an existing entry. - fun addTransaction(transaction: SignedTransaction): Boolean -} +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/flows/FinalityFlow.kt b/core/src/main/kotlin/net/corda/flows/FinalityFlow.kt index e609c4f5ca..dfc3c3c20d 100644 --- a/core/src/main/kotlin/net/corda/flows/FinalityFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/FinalityFlow.kt @@ -74,16 +74,15 @@ class FinalityFlow(val transactions: Iterable, @Suspendable private fun notariseAndRecord(stxnsAndParties: List>>): List>> { - return stxnsAndParties.map { pair -> - val stx = pair.first + return stxnsAndParties.map { (stx, parties) -> val notarised = if (needsNotarySignature(stx)) { val notarySignatures = subFlow(NotaryFlow.Client(stx)) stx + notarySignatures } else { stx } - serviceHub.recordTransactions(listOf(notarised)) - Pair(notarised, pair.second) + serviceHub.recordTransactions(notarised) + Pair(notarised, parties) } } @@ -101,8 +100,7 @@ class FinalityFlow(val transactions: Iterable, } private fun lookupParties(ltxns: List>): List>> { - return ltxns.map { pair -> - val (stx, ltx) = pair + return ltxns.map { (stx, ltx) -> // Calculate who is meant to see the results based on the participants involved. val keys = ltx.outputs.flatMap { it.data.participants } + ltx.inputs.flatMap { it.state.data.participants } // TODO: Is it safe to drop participants we don't know how to contact? Does not knowing how to contact them count as a reason to fail? diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt index eb30dd855e..38289537f1 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -178,14 +178,14 @@ class ContractUpgradeFlowTest { mockNet.runNetwork() val stx = result.getOrThrow().stx val stateAndRef = stx.tx.outRef(0) - val baseState = a.database.transaction { a.vault.unconsumedStates().single() } + val baseState = a.database.transaction { a.services.vaultService.unconsumedStates().single() } assertTrue(baseState.state.data is Cash.State, "Contract state is old version.") // Starts contract upgrade flow. val upgradeResult = a.services.startFlow(ContractUpgradeFlow(stateAndRef, CashV2::class.java)).resultFuture mockNet.runNetwork() upgradeResult.getOrThrow() // Get contract state from the vault. - val firstState = a.database.transaction { a.vault.unconsumedStates().single() } + val firstState = a.database.transaction { a.services.vaultService.unconsumedStates().single() } assertTrue(firstState.state.data is CashV2.State, "Contract state is upgraded to the new version.") assertEquals(Amount(1000000, USD).`issued by`(a.info.legalIdentity.ref(1)), (firstState.state.data as CashV2.State).amount, "Upgraded cash contain the correct amount.") assertEquals>(listOf(a.info.legalIdentity), (firstState.state.data as CashV2.State).owners, "Upgraded cash belongs to the right owner.") diff --git a/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt b/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt index eada1d6187..b7af9399ef 100644 --- a/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt @@ -33,8 +33,8 @@ class CashPaymentFlowTests { bankOfCorda = bankOfCordaNode.info.legalIdentity notaryNode.registerInitiatedFlow(TxKeyFlow.Provider::class.java) - notaryNode.identity.registerIdentity(bankOfCordaNode.info.legalIdentityAndCert) - bankOfCordaNode.identity.registerIdentity(notaryNode.info.legalIdentityAndCert) + notaryNode.services.identityService.registerIdentity(bankOfCordaNode.info.legalIdentityAndCert) + bankOfCordaNode.services.identityService.registerIdentity(notaryNode.info.legalIdentityAndCert) val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, ref, bankOfCorda, notary)).resultFuture diff --git a/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt index 0669214cbf..c170de0647 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt @@ -5,8 +5,8 @@ import net.corda.core.contracts.DummyContract import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef import net.corda.core.contracts.TransactionType -import net.corda.core.identity.Party import net.corda.core.getOrThrow +import net.corda.core.identity.Party import net.corda.core.map import net.corda.core.utilities.DUMMY_BANK_A import net.corda.flows.NotaryError @@ -26,28 +26,28 @@ class RaftNotaryServiceTests : NodeBasedTest() { @Test fun `detect double spend`() { - val (masterNode, alice) = Futures.allAsList( - startNotaryCluster(notaryName, 3).map { it.first() }, - startNode(DUMMY_BANK_A.name) + val (bankA) = Futures.allAsList( + startNode(DUMMY_BANK_A.name), + startNotaryCluster(notaryName, 3).map { it.first() } ).getOrThrow() - val notaryParty = alice.netMapCache.getNotary(notaryName)!! + val notaryParty = bankA.services.networkMapCache.getNotary(notaryName)!! - val inputState = issueState(alice, notaryParty) + val inputState = issueState(bankA, notaryParty) val firstTxBuilder = TransactionType.General.Builder(notaryParty).withItems(inputState) - val firstSpendTx = alice.services.signInitialTransaction(firstTxBuilder) + val firstSpendTx = bankA.services.signInitialTransaction(firstTxBuilder) - val firstSpend = alice.services.startFlow(NotaryFlow.Client(firstSpendTx)) + val firstSpend = bankA.services.startFlow(NotaryFlow.Client(firstSpendTx)) firstSpend.resultFuture.getOrThrow() val secondSpendBuilder = TransactionType.General.Builder(notaryParty).withItems(inputState).run { - val dummyState = DummyContract.SingleOwnerState(0, alice.info.legalIdentity) + val dummyState = DummyContract.SingleOwnerState(0, bankA.info.legalIdentity) addOutputState(dummyState) this } - val secondSpendTx = alice.services.signInitialTransaction(secondSpendBuilder) - val secondSpend = alice.services.startFlow(NotaryFlow.Client(secondSpendTx)) + val secondSpendTx = bankA.services.signInitialTransaction(secondSpendBuilder) + val secondSpend = bankA.services.startFlow(NotaryFlow.Client(secondSpendTx)) val ex = assertFailsWith(NotaryException::class) { secondSpend.resultFuture.getOrThrow() } val error = ex.error as NotaryError.Conflict @@ -58,7 +58,7 @@ class RaftNotaryServiceTests : NodeBasedTest() { return node.database.transaction { val builder = DummyContract.generateInitial(Random().nextInt(), notary, node.info.legalIdentity.ref(0)) val stx = node.services.signInitialTransaction(builder) - node.services.recordTransactions(listOf(stx)) + node.services.recordTransactions(stx) StateAndRef(builder.outputStates().first(), StateRef(stx.id, 0)) } } diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 65d1c7052e..9a34e809d0 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -20,7 +20,6 @@ import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.* import net.corda.core.node.services.* import net.corda.core.node.services.NetworkMapCache.MapChange -import net.corda.core.node.services.NotaryService import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.deserialize @@ -42,6 +41,7 @@ import net.corda.node.services.messaging.MessagingService import net.corda.node.services.messaging.sendRequest import net.corda.node.services.network.InMemoryNetworkMapCache import net.corda.node.services.network.NetworkMapService +import net.corda.node.services.network.NetworkMapService.RegistrationRequest import net.corda.node.services.network.NetworkMapService.RegistrationResponse import net.corda.node.services.network.NodeRegistration import net.corda.node.services.network.PersistentNetworkMapService @@ -122,78 +122,18 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, private val flowFactories = ConcurrentHashMap>, InitiatedFlowFactory<*>>() protected val partyKeys = mutableSetOf() - val services = object : ServiceHubInternal { - override val attachments: AttachmentStorage get() = this@AbstractNode.attachments - override val uploaders: List get() = this@AbstractNode.uploaders - override val stateMachineRecordedTransactionMapping: StateMachineRecordedTransactionMappingStorage - get() = this@AbstractNode.transactionMappings - override val validatedTransactions: TransactionStorage get() = this@AbstractNode.transactions - override val networkService: MessagingService get() = network - override val networkMapCache: NetworkMapCacheInternal get() = netMapCache - override val vaultService: VaultService get() = vault - override val vaultQueryService: VaultQueryService get() = vaultQuery - override val keyManagementService: KeyManagementService get() = keyManagement - override val identityService: IdentityService get() = identity - override val schedulerService: SchedulerService get() = scheduler - override val clock: Clock get() = platformClock - override val myInfo: NodeInfo get() = info - override val schemaService: SchemaService get() = schemas - override val transactionVerifierService: TransactionVerifierService get() = txVerifierService - override val auditService: AuditService get() = this@AbstractNode.auditService - override val database: Database get() = this@AbstractNode.database - override val configuration: NodeConfiguration get() = this@AbstractNode.configuration - - override fun cordaService(type: Class): T { - require(type.isAnnotationPresent(CordaService::class.java)) { "${type.name} is not a Corda service" } - return cordappServices.getInstance(type) ?: throw IllegalArgumentException("Corda service ${type.name} does not exist") - } - - override val rpcFlows: List>> get() = this@AbstractNode.rpcFlows - - // Internal only - override val monitoringService: MonitoringService = MonitoringService(MetricRegistry()) - - override fun startFlow(logic: FlowLogic, flowInitiator: FlowInitiator): FlowStateMachineImpl { - return serverThread.fetchFrom { smm.add(logic, flowInitiator) } - } - - override fun getFlowFactory(initiatingFlowClass: Class>): InitiatedFlowFactory<*>? { - return flowFactories[initiatingFlowClass] - } - - override fun recordTransactions(txs: Iterable) { - database.transaction { - super.recordTransactions(txs) - } - } - } - - open fun findMyLocation(): WorldMapLocation? { - return configuration.myLegalName.locationOrNull?.let { CityDatabase[it] } - } + val services: ServiceHubInternal get() = _services + private lateinit var _services: ServiceHubInternalImpl lateinit var info: NodeInfo lateinit var checkpointStorage: CheckpointStorage lateinit var smm: StateMachineManager lateinit var attachments: NodeAttachmentService - lateinit var transactions: TransactionStorage - lateinit var transactionMappings: StateMachineRecordedTransactionMappingStorage - lateinit var uploaders: List - lateinit var vault: VaultService - lateinit var vaultQuery: VaultQueryService - lateinit var keyManagement: KeyManagementService var inNodeNetworkMapService: NetworkMapService? = null - lateinit var txVerifierService: TransactionVerifierService - lateinit var identity: IdentityService lateinit var network: MessagingService - lateinit var netMapCache: NetworkMapCacheInternal - lateinit var scheduler: NodeSchedulerService - lateinit var schemas: SchemaService - lateinit var auditService: AuditService protected val runOnStop = ArrayList<() -> Any?>() lateinit var database: Database protected var dbCloser: (() -> Any?)? = null - private lateinit var rpcFlows: List>> var isPreviousCheckpointsPresent = false private set @@ -215,6 +155,10 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, /** The implementation of the [CordaRPCOps] interface used by this node. */ open val rpcOps: CordaRPCOps by lazy { CordaRPCOpsImpl(services, smm, database) } // Lazy to avoid init ordering issue with the SMM. + open fun findMyLocation(): WorldMapLocation? { + return configuration.myLegalName.locationOrNull?.let { CityDatabase[it] } + } + open fun start(): AbstractNode { require(!started) { "Node has already been started" } @@ -233,8 +177,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, // Do all of this in a database transaction so anything that might need a connection has one. initialiseDatabasePersistence { - val keyStoreWrapper = KeyStoreWrapper(configuration.trustStoreFile, configuration.trustStorePassword) - val tokenizableServices = makeServices(keyStoreWrapper) + val tokenizableServices = makeServices() smm = StateMachineManager(services, checkpointStorage, @@ -266,9 +209,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, if (scanResult != null) { installCordaServices(scanResult) registerInitiatedFlows(scanResult) - rpcFlows = findRPCFlows(scanResult) - } else { - rpcFlows = emptyList() + findRPCFlows(scanResult) } // TODO: Investigate having class path scanning find this flow @@ -283,7 +224,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, smm.start() // Shut down the SMM so no Fibers are scheduled. runOnStop += { smm.stop(acceptableLiveFiberCountOnStop()) } - scheduler.start() + _services.schedulerService.start() } started = true return this @@ -301,7 +242,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, } } - return scanResult.getClassesWithAnnotation(SerializeAsToken::class, CordaService::class) + scanResult.getClassesWithAnnotation(SerializeAsToken::class, CordaService::class) .filter { val serviceType = getServiceType(it) if (serviceType != null && info.serviceIdentities(serviceType).isEmpty()) { @@ -434,19 +375,21 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, return observable } - private fun findRPCFlows(scanResult: ScanResult): List>> { + private fun findRPCFlows(scanResult: ScanResult) { fun Class>.isUserInvokable(): Boolean { return isPublic(modifiers) && !isLocalClass && !isAnonymousClass && (!isMemberClass || isStatic(modifiers)) } - return scanResult.getClassesWithAnnotation(FlowLogic::class, StartableByRPC::class).filter { it.isUserInvokable() } + - // Add any core flows here - listOf( - ContractUpgradeFlow::class.java, - // TODO Remove all Cash flows from default list once they are split into separate CorDapp. - CashIssueFlow::class.java, - CashExitFlow::class.java, - CashPaymentFlow::class.java) + _services.rpcFlows += scanResult + .getClassesWithAnnotation(FlowLogic::class, StartableByRPC::class) + .filter { it.isUserInvokable() } + + // Add any core flows here + listOf( + ContractUpgradeFlow::class.java, + // TODO Remove all Cash flows from default list once they are split into separate CorDapp. + CashIssueFlow::class.java, + CashExitFlow::class.java, + CashPaymentFlow::class.java) } /** @@ -476,36 +419,20 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, * Builds node internal, advertised, and plugin services. * Returns a list of tokenizable services to be added to the serialisation context. */ - private fun makeServices(keyStoreWrapper: KeyStoreWrapper): MutableList { - val keyStore = keyStoreWrapper.keyStore - attachments = createAttachmentStorage() - transactions = createTransactionStorage() - transactionMappings = DBTransactionMappingStorage() + private fun makeServices(): MutableList { checkpointStorage = DBCheckpointStorage() - netMapCache = InMemoryNetworkMapCache(services) + _services = ServiceHubInternalImpl() + attachments = createAttachmentStorage() network = makeMessagingService() - schemas = makeSchemaService() - vault = makeVaultService(configuration.dataSourceProperties) - vaultQuery = makeVaultQueryService(schemas) - txVerifierService = makeTransactionVerifierService() - auditService = DummyAuditService() - info = makeInfo() - identity = makeIdentityService(keyStore.getCertificate(X509Utilities.CORDA_ROOT_CA)!! as X509Certificate, - keyStoreWrapper.certificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA), - info.legalIdentityAndCert) - // Place the long term identity key in the KMS. Eventually, this is likely going to be separated again because - // the KMS is meant for derived temporary keys used in transactions, and we're not supposed to sign things with - // the identity key. But the infrastructure to make that easy isn't here yet. - keyManagement = makeKeyManagementService(identity) - scheduler = NodeSchedulerService(services, database, unfinishedSchedules = busyNodeLatch) - val tokenizableServices = mutableListOf(attachments, network, vault, vaultQuery, keyManagement, identity, platformClock, scheduler) + val tokenizableServices = mutableListOf(attachments, network, services.vaultService, services.vaultQueryService, + services.keyManagementService, services.identityService, platformClock, services.schedulerService) makeAdvertisedServices(tokenizableServices) return tokenizableServices } - protected open fun createTransactionStorage(): TransactionStorage = DBTransactionStorage() + protected open fun makeTransactionStorage(): WritableTransactionStorage = DBTransactionStorage() private fun scanCordapps(): ScanResult? { val scanPackage = System.getProperty("net.corda.node.cordapp.scan.package") @@ -560,14 +487,15 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, } private fun initUploaders() { - uploaders = listOf(attachments) + cordappServices.values.filterIsInstance(AcceptsFileUpload::class.java) + _services.uploaders += attachments + cordappServices.values.filterIsInstanceTo(_services.uploaders, AcceptsFileUpload::class.java) } private fun makeVaultObservers() { - VaultSoftLockManager(vault, smm) + VaultSoftLockManager(services.vaultService, smm) CashBalanceAsMetricsObserver(services, database) ScheduledActivityObserver(services) - HibernateObserver(vault.rawUpdates, HibernateConfiguration(schemas)) + HibernateObserver(services.vaultService.rawUpdates, HibernateConfiguration(services.schemaService)) } private fun makeInfo(): NodeInfo { @@ -684,7 +612,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val expires = instant + NetworkMapService.DEFAULT_EXPIRATION_PERIOD val reg = NodeRegistration(info, instant.toEpochMilli(), ADD, expires) val legalIdentityKey = obtainLegalIdentityKey() - val request = NetworkMapService.RegistrationRequest(reg.toWire(keyManagement, legalIdentityKey.public), network.myAddress) + val request = RegistrationRequest(reg.toWire(services.keyManagementService, legalIdentityKey.public), network.myAddress) return network.sendRequest(NetworkMapService.REGISTER_TOPIC, request, networkMapAddress) } @@ -735,7 +663,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, .toTypedArray() val service = InMemoryIdentityService(setOf(info.legalIdentityAndCert), trustRoot = trustRoot, caCertificates = *caCertificates) services.networkMapCache.partyNodes.forEach { service.registerIdentity(it.legalIdentityAndCert) } - netMapCache.changed.subscribe { mapChange -> + services.networkMapCache.changed.subscribe { mapChange -> // TODO how should we handle network map removal if (mapChange is MapChange.Added) { service.registerIdentity(mapChange.node.legalIdentityAndCert) @@ -744,13 +672,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, return service } - // TODO: sort out ordering of open & protected modifiers of functions in this class. - protected open fun makeVaultService(dataSourceProperties: Properties): VaultService = NodeVaultService(services, dataSourceProperties) - - protected open fun makeVaultQueryService(schemas: SchemaService): VaultQueryService = HibernateVaultQueryImpl(HibernateConfiguration(schemas), vault.updatesPublisher) - - protected open fun makeSchemaService(): SchemaService = NodeSchemaService(pluginRegistries.flatMap { it.requiredSchemas }.toSet()) - protected abstract fun makeTransactionVerifierService(): TransactionVerifierService open fun stop() { @@ -844,6 +765,60 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val attachmentsDir = (configuration.baseDirectory / "attachments").createDirectories() return NodeAttachmentService(attachmentsDir, configuration.dataSourceProperties, services.monitoringService.metrics) } + + private inner class ServiceHubInternalImpl : ServiceHubInternal, SingletonSerializeAsToken() { + override val rpcFlows = ArrayList>>() + override val uploaders = ArrayList() + override val stateMachineRecordedTransactionMapping = DBTransactionMappingStorage() + override val auditService = DummyAuditService() + override val monitoringService = MonitoringService(MetricRegistry()) + override val validatedTransactions = makeTransactionStorage() + override val transactionVerifierService by lazy { makeTransactionVerifierService() } + override val networkMapCache by lazy { InMemoryNetworkMapCache(this) } + override val vaultService by lazy { NodeVaultService(this, configuration.dataSourceProperties) } + override val vaultQueryService by lazy { + HibernateVaultQueryImpl(HibernateConfiguration(schemaService), vaultService.updatesPublisher) + } + // Place the long term identity key in the KMS. Eventually, this is likely going to be separated again because + // the KMS is meant for derived temporary keys used in transactions, and we're not supposed to sign things with + // the identity key. But the infrastructure to make that easy isn't here yet. + override val keyManagementService by lazy { makeKeyManagementService(identityService) } + override val schedulerService by lazy { NodeSchedulerService(this, unfinishedSchedules = busyNodeLatch) } + override val identityService by lazy { + val keyStoreWrapper = KeyStoreWrapper(configuration.trustStoreFile, configuration.trustStorePassword) + makeIdentityService( + keyStoreWrapper.keyStore.getCertificate(X509Utilities.CORDA_ROOT_CA)!! as X509Certificate, + keyStoreWrapper.certificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA), + info.legalIdentityAndCert) + } + override val attachments: AttachmentStorage get() = this@AbstractNode.attachments + override val networkService: MessagingService get() = network + override val clock: Clock get() = platformClock + override val myInfo: NodeInfo get() = info + override val schemaService by lazy { NodeSchemaService(pluginRegistries.flatMap { it.requiredSchemas }.toSet()) } + override val database: Database get() = this@AbstractNode.database + override val configuration: NodeConfiguration get() = this@AbstractNode.configuration + + override fun cordaService(type: Class): T { + require(type.isAnnotationPresent(CordaService::class.java)) { "${type.name} is not a Corda service" } + return cordappServices.getInstance(type) ?: throw IllegalArgumentException("Corda service ${type.name} does not exist") + } + + override fun startFlow(logic: FlowLogic, flowInitiator: FlowInitiator): FlowStateMachineImpl { + return serverThread.fetchFrom { smm.add(logic, flowInitiator) } + } + + override fun getFlowFactory(initiatingFlowClass: Class>): InitiatedFlowFactory<*>? { + return flowFactories[initiatingFlowClass] + } + + override fun recordTransactions(txs: Iterable) { + database.transaction { + super.recordTransactions(txs) + } + } + } + } private class KeyStoreWrapper(val keyStore: KeyStore, val storePath: Path, private val storePassword: String) { diff --git a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt index ef427d4b65..861ff1db34 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt @@ -72,7 +72,7 @@ interface ServiceHubInternal : PluginServiceHub { * The signatures aren't technically needed after that point, but we keep them around so that we can relay * the transaction data to other nodes that need it. */ - override val validatedTransactions: TransactionStorage + override val validatedTransactions: WritableTransactionStorage val stateMachineRecordedTransactionMapping: StateMachineRecordedTransactionMappingStorage val monitoringService: MonitoringService val schemaService: SchemaService @@ -89,8 +89,9 @@ interface ServiceHubInternal : PluginServiceHub { val uploaders: List override fun recordTransactions(txs: Iterable) { - val stateMachineRunId = FlowStateMachineImpl.currentStateMachine()?.id val recordedTransactions = txs.filter { validatedTransactions.addTransaction(it) } + require(recordedTransactions.isNotEmpty()) { "No transactions passed in for recording" } + val stateMachineRunId = FlowStateMachineImpl.currentStateMachine()?.id if (stateMachineRunId != null) { recordedTransactions.forEach { stateMachineRecordedTransactionMapping.addMapping(stateMachineRunId, it.id) @@ -135,6 +136,20 @@ interface ServiceHubInternal : PluginServiceHub { fun getFlowFactory(initiatingFlowClass: Class>): InitiatedFlowFactory<*>? } +/** + * Thread-safe storage of transactions. + */ +interface WritableTransactionStorage : TransactionStorage { + /** + * Add a new transaction to the store. If the store already has a transaction with the same id it will be + * overwritten. + * @param transaction The transaction to be recorded. + * @return true if the transaction was recorded successfully, false if it was already recorded. + */ + // TODO: Throw an exception if trying to add a transaction with fewer signatures than an existing entry. + fun addTransaction(transaction: SignedTransaction): Boolean +} + /** * This is the interface to storage storing state machine -> recorded tx mappings. Any time a transaction is recorded * during a flow run [addMapping] should be called. diff --git a/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt b/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt index 6faa38d735..b69906afbe 100644 --- a/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt +++ b/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt @@ -17,7 +17,6 @@ import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl import net.corda.node.utilities.* import org.apache.activemq.artemis.utils.ReusableLatch -import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.statements.InsertStatement import java.time.Instant @@ -43,7 +42,6 @@ import javax.annotation.concurrent.ThreadSafe */ @ThreadSafe class NodeSchedulerService(private val services: ServiceHubInternal, - private val database: Database, private val schedulerTimerExecutor: Executor = Executors.newSingleThreadExecutor(), private val unfinishedSchedules: ReusableLatch = ReusableLatch()) : SchedulerService, SingletonSerializeAsToken() { @@ -159,7 +157,7 @@ class NodeSchedulerService(private val services: ServiceHubInternal, } private fun onTimeReached(scheduledState: ScheduledStateRef) { - database.transaction { + services.database.transaction { val scheduledFlow = getScheduledFlow(scheduledState) if (scheduledFlow != null) { // TODO Because the flow is executed asynchronously, there is a small window between this tx we're in diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt index 149d6db12f..219865b00e 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt @@ -4,9 +4,9 @@ import com.google.common.annotations.VisibleForTesting import net.corda.core.bufferUntilSubscribed import net.corda.core.crypto.SecureHash import net.corda.core.messaging.DataFeed -import net.corda.core.node.services.TransactionStorage import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.SignedTransaction +import net.corda.node.services.api.WritableTransactionStorage import net.corda.node.utilities.* import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.exposedLogger @@ -15,7 +15,7 @@ import rx.Observable import rx.subjects.PublishSubject import java.util.Collections.synchronizedMap -class DBTransactionStorage : TransactionStorage, SingletonSerializeAsToken() { +class DBTransactionStorage : WritableTransactionStorage, SingletonSerializeAsToken() { private object Table : JDBCHashedTable("${NODE_DATABASE_PREFIX}transactions") { val txId = secureHash("tx_id") val transaction = blob("transaction") diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt index 06f60228c1..bbb4ffc340 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt @@ -8,10 +8,7 @@ import com.google.common.hash.HashingInputStream import com.google.common.io.CountingInputStream import net.corda.core.contracts.AbstractAttachment import net.corda.core.contracts.Attachment -import net.corda.core.createDirectory import net.corda.core.crypto.SecureHash -import net.corda.core.div -import net.corda.core.extractZipFile import net.corda.core.isDirectory import net.corda.core.node.services.AttachmentStorage import net.corda.core.serialization.* @@ -35,7 +32,7 @@ import javax.annotation.concurrent.ThreadSafe * Stores attachments in H2 database. */ @ThreadSafe -class NodeAttachmentService(override var storePath: Path, dataSourceProperties: Properties, metrics: MetricRegistry) +class NodeAttachmentService(val storePath: Path, dataSourceProperties: Properties, metrics: MetricRegistry) : AttachmentStorage, AcceptsFileUpload, SingletonSerializeAsToken() { companion object { private val log = loggerFor() @@ -48,7 +45,6 @@ class NodeAttachmentService(override var storePath: Path, dataSourceProperties: var checkAttachmentsOnLoad = true private val attachmentCount = metrics.counter("Attachments") - @Volatile override var automaticallyExtractAttachments = false init { require(storePath.isDirectory()) { "$storePath must be a directory" } @@ -183,19 +179,6 @@ class NodeAttachmentService(override var storePath: Path, dataSourceProperties: log.info("Stored new attachment $id") - if (automaticallyExtractAttachments) { - val extractTo = storePath / "$id.jar" - try { - extractTo.createDirectory() - extractZipFile(ByteArrayInputStream(bytes), extractTo) - } catch(e: FileAlreadyExistsException) { - log.trace("Did not extract attachment jar to directory because it already exists") - } catch(e: Exception) { - log.error("Failed to extract attachment jar $id, ", e) - // TODO: Delete the extractTo directory here. - } - } - return id } diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index 2aebbe86b2..79e1dae83c 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -22,7 +22,6 @@ import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.messaging.StateMachineTransactionMapping import net.corda.core.node.NodeInfo import net.corda.core.node.services.ServiceInfo -import net.corda.core.node.services.TransactionStorage import net.corda.core.node.services.Vault import net.corda.core.serialization.serialize import net.corda.core.transactions.SignedTransaction @@ -32,6 +31,7 @@ import net.corda.core.utilities.* import net.corda.flows.TwoPartyTradeFlow.Buyer import net.corda.flows.TwoPartyTradeFlow.Seller import net.corda.node.internal.AbstractNode +import net.corda.node.services.api.WritableTransactionStorage import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.persistence.DBTransactionStorage import net.corda.node.services.persistence.checkpoints @@ -153,7 +153,7 @@ class TwoPartyTradeFlowTests { val cashLockId = UUID.randomUUID() bobNode.database.transaction { // lock the cash states with an arbitrary lockId (to prevent the Buyer flow from claiming the states) - bobNode.vault.softLockReserve(cashLockId, cashStates.states.map { it.ref }.toSet()) + bobNode.services.vaultService.softLockReserve(cashLockId, cashStates.states.map { it.ref }.toSet()) } val (bobStateMachine, aliceResult) = runBuyerAndSeller(notaryNode, aliceNode, bobNode, @@ -284,8 +284,8 @@ class TwoPartyTradeFlowTests { entropyRoot: BigInteger): MockNetwork.MockNode { return object : MockNetwork.MockNode(config, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) { // That constructs a recording tx storage - override fun createTransactionStorage(): TransactionStorage { - return RecordingTransactionStorage(database, super.createTransactionStorage()) + override fun makeTransactionStorage(): WritableTransactionStorage { + return RecordingTransactionStorage(database, super.makeTransactionStorage()) } } } @@ -311,7 +311,7 @@ class TwoPartyTradeFlowTests { attachment(ByteArrayInputStream(stream.toByteArray())) } - val extraKey = bobNode.keyManagement.keys.single() + val extraKey = bobNode.services.keyManagementService.keys.single() val bobsFakeCash = fillUpForBuyer(false, AnonymousParty(extraKey), DUMMY_CASH_ISSUER.party, notaryNode.info.notaryIdentity).second @@ -410,7 +410,7 @@ class TwoPartyTradeFlowTests { attachment(ByteArrayInputStream(stream.toByteArray())) } - val bobsKey = bobNode.keyManagement.keys.single() + val bobsKey = bobNode.services.keyManagementService.keys.single() val bobsFakeCash = fillUpForBuyer(false, AnonymousParty(bobsKey), DUMMY_CASH_ISSUER.party, notaryNode.info.notaryIdentity).second @@ -675,7 +675,7 @@ class TwoPartyTradeFlowTests { } - class RecordingTransactionStorage(val database: Database, val delegate: TransactionStorage) : TransactionStorage { + class RecordingTransactionStorage(val database: Database, val delegate: WritableTransactionStorage) : WritableTransactionStorage { override fun track(): DataFeed, SignedTransaction> { return database.transaction { delegate.track() diff --git a/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt b/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt index 406818e3c6..037368b2f3 100644 --- a/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt +++ b/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt @@ -18,19 +18,20 @@ import net.corda.node.services.transactions.InMemoryTransactionVerifierService import net.corda.testing.MOCK_IDENTITY_SERVICE import net.corda.testing.node.MockAttachmentStorage import net.corda.testing.node.MockNetworkMapCache -import org.jetbrains.exposed.sql.Database import net.corda.testing.node.MockStateMachineRecordedTransactionMappingStorage import net.corda.testing.node.MockTransactionStorage +import org.jetbrains.exposed.sql.Database import java.time.Clock open class MockServiceHubInternal( + override val database: Database, val customVault: VaultService? = null, val customVaultQuery: VaultQueryService? = null, val keyManagement: KeyManagementService? = null, val network: MessagingService? = null, val identity: IdentityService? = MOCK_IDENTITY_SERVICE, override val attachments: AttachmentStorage = MockAttachmentStorage(), - override val validatedTransactions: TransactionStorage = MockTransactionStorage(), + override val validatedTransactions: WritableTransactionStorage = MockTransactionStorage(), override val uploaders: List = listOf(), override val stateMachineRecordedTransactionMapping: StateMachineRecordedTransactionMappingStorage = MockStateMachineRecordedTransactionMappingStorage(), val mapCache: NetworkMapCacheInternal? = null, @@ -59,8 +60,6 @@ open class MockServiceHubInternal( get() = overrideClock ?: throw UnsupportedOperationException() override val myInfo: NodeInfo get() = throw UnsupportedOperationException() - override val database: Database - get() = throw UnsupportedOperationException() override val configuration: NodeConfiguration get() = throw UnsupportedOperationException() override val monitoringService: MonitoringService = MonitoringService(MetricRegistry()) diff --git a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt index 866a7cb3d2..206cccccb4 100644 --- a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt @@ -135,7 +135,7 @@ class NotaryChangeTests { addOutputState(stateB, notary, encumbrance = 1) // Encumbered by stateC } val stx = node.services.signInitialTransaction(tx) - node.services.recordTransactions(listOf(stx)) + node.services.recordTransactions(stx) return tx.toWireTransaction() } @@ -152,7 +152,7 @@ fun issueState(node: AbstractNode, notaryNode: AbstractNode): StateAndRef<*> { val tx = DummyContract.generateInitial(Random().nextInt(), notaryNode.info.notaryIdentity, node.info.legalIdentity.ref(0)) val signedByNode = node.services.signInitialTransaction(tx) val stx = notaryNode.services.addSignature(signedByNode, notaryNode.services.notaryIdentityKey) - node.services.recordTransactions(listOf(stx)) + node.services.recordTransactions(stx) return StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0)) } @@ -163,8 +163,8 @@ fun issueMultiPartyState(nodeA: AbstractNode, nodeB: AbstractNode, notaryNode: A val signedByA = nodeA.services.signInitialTransaction(tx) val signedByAB = nodeB.services.addSignature(signedByA) val stx = notaryNode.services.addSignature(signedByAB, notaryNode.services.notaryIdentityKey) - nodeA.services.recordTransactions(listOf(stx)) - nodeB.services.recordTransactions(listOf(stx)) + nodeA.services.recordTransactions(stx) + nodeB.services.recordTransactions(stx) val stateAndRef = StateAndRef(state, StateRef(stx.id, 0)) return stateAndRef } @@ -173,6 +173,6 @@ fun issueInvalidState(node: AbstractNode, notary: Party): StateAndRef<*> { val tx = DummyContract.generateInitial(Random().nextInt(), notary, node.info.legalIdentity.ref(0)) tx.addTimeWindow(Instant.now(), 30.seconds) val stx = node.services.signInitialTransaction(tx) - node.services.recordTransactions(listOf(stx)) + node.services.recordTransactions(stx) return StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0)) } diff --git a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt index 71754dd4d5..a17299eea2 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt @@ -87,11 +87,11 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { InMemoryMessagingNetwork.PeerHandle(0, nullIdentity), AffinityExecutor.ServiceAffinityExecutor("test", 1), database) - services = object : MockServiceHubInternal(overrideClock = testClock, keyManagement = kms, network = mockMessagingService), TestReference { + services = object : MockServiceHubInternal(database, overrideClock = testClock, keyManagement = kms, network = mockMessagingService), TestReference { override val vaultService: VaultService = NodeVaultService(this, dataSourceProps) override val testReference = this@NodeSchedulerServiceTest } - scheduler = NodeSchedulerService(services, database, schedulerGatedExecutor) + scheduler = NodeSchedulerService(services, schedulerGatedExecutor) smmExecutor = AffinityExecutor.ServiceAffinityExecutor("test", 1) val mockSMM = StateMachineManager(services, DBCheckpointStorage(), smmExecutor, database) mockSMM.changes.subscribe { change -> diff --git a/node/src/test/kotlin/net/corda/node/services/network/InMemoryNetworkMapCacheTest.kt b/node/src/test/kotlin/net/corda/node/services/network/InMemoryNetworkMapCacheTest.kt index 79239ca0ed..05bfe43260 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/InMemoryNetworkMapCacheTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/InMemoryNetworkMapCacheTest.kt @@ -38,13 +38,13 @@ class InMemoryNetworkMapCacheTest { mockNet.runNetwork() // Node A currently knows only about itself, so this returns node A - assertEquals(nodeA.netMapCache.getNodeByLegalIdentityKey(nodeA.info.legalIdentity.owningKey), nodeA.info) + assertEquals(nodeA.services.networkMapCache.getNodeByLegalIdentityKey(nodeA.info.legalIdentity.owningKey), nodeA.info) nodeA.database.transaction { - nodeA.netMapCache.addNode(nodeB.info) + nodeA.services.networkMapCache.addNode(nodeB.info) } // The details of node B write over those for node A - assertEquals(nodeA.netMapCache.getNodeByLegalIdentityKey(nodeA.info.legalIdentity.owningKey), nodeB.info) + assertEquals(nodeA.services.networkMapCache.getNodeByLegalIdentityKey(nodeA.info.legalIdentity.owningKey), nodeB.info) } @Test diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt index 61c708f875..45ed3fc9d5 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt @@ -131,7 +131,7 @@ class NotaryServiceTests { val tx = DummyContract.generateInitial(Random().nextInt(), notaryNode.info.notaryIdentity, node.info.legalIdentity.ref(0)) val signedByNode = node.services.signInitialTransaction(tx) val stx = notaryNode.services.addSignature(signedByNode, notaryNode.services.notaryIdentityKey) - node.services.recordTransactions(listOf(stx)) + node.services.recordTransactions(stx) return StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0)) } } diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt index d2d9080483..3d6f495650 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt @@ -82,7 +82,7 @@ class ValidatingNotaryServiceTests { val tx = DummyContract.generateInitial(Random().nextInt(), notaryNode.info.notaryIdentity, node.info.legalIdentity.ref(0)) val signedByNode = node.services.signInitialTransaction(tx) val stx = notaryNode.services.addSignature(signedByNode, notaryNode.services.notaryIdentityKey) - node.services.recordTransactions(listOf(stx)) + node.services.recordTransactions(stx) return StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0)) } } diff --git a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt index ed0ef9e133..1f9297c522 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt @@ -405,7 +405,7 @@ class NodeVaultServiceTest { } val usefulTX = megaCorpServices.signInitialTransaction(usefulBuilder) - services.recordTransactions(listOf(usefulTX)) + services.recordTransactions(usefulTX) vaultSvc.addNoteToTransaction(usefulTX.id, "USD Sample Note 1") vaultSvc.addNoteToTransaction(usefulTX.id, "USD Sample Note 2") @@ -418,7 +418,7 @@ class NodeVaultServiceTest { } val anotherTX = megaCorpServices.signInitialTransaction(anotherBuilder) - services.recordTransactions(listOf(anotherTX)) + services.recordTransactions(anotherTX) vaultSvc.addNoteToTransaction(anotherTX.id, "GPB Sample Note 1") assertEquals(1, vaultSvc.getTransactionNotes(anotherTX.id).count()) diff --git a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt index 9eec30633d..baae371d8c 100644 --- a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt +++ b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt @@ -167,7 +167,7 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, val serviceProviders: List = listOf(notary, ratesOracle, networkMap) val banks: List = bankFactory.createAll() - val clocks = (serviceProviders + regulators + banks).map { it.services.clock as TestClock } + val clocks = (serviceProviders + regulators + banks).map { it.platformClock as TestClock } // These are used from the network visualiser tool. private val _allFlowSteps = PublishSubject.create>() diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/BuyerFlow.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/BuyerFlow.kt index 1bcc64ef8b..10f2e030f0 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/BuyerFlow.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/BuyerFlow.kt @@ -4,7 +4,6 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.CommercialPaper import net.corda.core.contracts.Amount import net.corda.core.contracts.TransactionGraphSearch -import net.corda.core.div import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatedBy import net.corda.core.identity.Party @@ -39,7 +38,7 @@ class BuyerFlow(val otherParty: Party) : FlowLogic() { // This invokes the trading flow and out pops our finished transaction. val tradeTX: SignedTransaction = subFlow(buyer) // TODO: This should be moved into the flow itself. - serviceHub.recordTransactions(listOf(tradeTX)) + serviceHub.recordTransactions(tradeTX) println("Purchase complete - we are a happy customer! Final transaction is: " + "\n\n${Emoji.renderIfSupported(tradeTX.tx)}") @@ -61,18 +60,11 @@ class BuyerFlow(val otherParty: Party) : FlowLogic() { val cpIssuance = search.call().single() // Buyer will fetch the attachment from the seller automatically when it resolves the transaction. - // For demo purposes just extract attachment jars when saved to disk, so the user can explore them. - val attachmentsPath = (serviceHub.attachments).let { - it.automaticallyExtractAttachments = true - it.storePath - } cpIssuance.attachments.first().let { - val p = attachmentsPath / "$it.jar" println(""" -The issuance of the commercial paper came with an attachment. You can find it expanded in this directory: -$p +The issuance of the commercial paper came with an attachment. You can find it in the attachments directory: $it.jar ${Emoji.renderIfSupported(cpIssuance)}""") } diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt index 2762a20236..2598929ba5 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -15,9 +15,11 @@ import net.corda.core.messaging.MessageRecipients import net.corda.core.messaging.RPCOps import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.CordaPluginRegistry -import net.corda.core.node.WorldMapLocation import net.corda.core.node.ServiceEntry -import net.corda.core.node.services.* +import net.corda.core.node.WorldMapLocation +import net.corda.core.node.services.IdentityService +import net.corda.core.node.services.KeyManagementService +import net.corda.core.node.services.ServiceInfo import net.corda.core.utilities.DUMMY_NOTARY_KEY import net.corda.core.utilities.getTestPartyAndCertificate import net.corda.core.utilities.loggerFor @@ -32,7 +34,6 @@ import net.corda.node.services.network.NetworkMapService import net.corda.node.services.transactions.InMemoryTransactionVerifierService import net.corda.node.services.transactions.SimpleNotaryService import net.corda.node.services.transactions.ValidatingNotaryService -import net.corda.node.services.vault.NodeVaultService import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor import net.corda.testing.MOCK_VERSION_INFO @@ -181,8 +182,6 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, trustRoot = trustRoot, caCertificates = *caCertificates) } - override fun makeVaultService(dataSourceProperties: Properties): VaultService = NodeVaultService(services, dataSourceProperties) - override fun makeKeyManagementService(identityService: IdentityService): KeyManagementService { return E2ETestKeyManagementService(identityService, partyKeys + (overrideServices?.values ?: emptySet())) } diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt index 30fe59eadc..4584f36316 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -16,6 +16,7 @@ import net.corda.core.utilities.DUMMY_CA import net.corda.core.utilities.getTestPartyAndCertificate import net.corda.flows.AnonymisedIdentity import net.corda.node.services.api.StateMachineRecordedTransactionMappingStorage +import net.corda.node.services.api.WritableTransactionStorage import net.corda.node.services.database.HibernateConfiguration import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.keys.freshCertificate @@ -35,8 +36,6 @@ import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.File import java.io.InputStream -import java.nio.file.Path -import java.nio.file.Paths import java.security.KeyPair import java.security.PrivateKey import java.security.PublicKey @@ -66,7 +65,7 @@ open class MockServices(vararg val keys: KeyPair) : ServiceHub { } override val attachments: AttachmentStorage = MockAttachmentStorage() - override val validatedTransactions: TransactionStorage = MockTransactionStorage() + override val validatedTransactions: WritableTransactionStorage = MockTransactionStorage() val stateMachineRecordedTransactionMapping: StateMachineRecordedTransactionMappingStorage = MockStateMachineRecordedTransactionMappingStorage() override final val identityService: IdentityService = InMemoryIdentityService(MOCK_IDENTITIES, trustRoot = DUMMY_CA.certificate) override val keyManagementService: KeyManagementService = MockKeyManagementService(identityService, *keys) @@ -124,10 +123,8 @@ class MockKeyManagementService(val identityService: IdentityService, } } -class MockAttachmentStorage : AttachmentStorage { +class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() { val files = HashMap() - override var automaticallyExtractAttachments = false - override var storePath: Path = Paths.get("") override fun openAttachment(id: SecureHash): Attachment? { val f = files[id] ?: return null @@ -159,7 +156,7 @@ class MockStateMachineRecordedTransactionMappingStorage( val storage: StateMachineRecordedTransactionMappingStorage = InMemoryStateMachineRecordedTransactionMappingStorage() ) : StateMachineRecordedTransactionMappingStorage by storage -open class MockTransactionStorage : TransactionStorage { +open class MockTransactionStorage : WritableTransactionStorage, SingletonSerializeAsToken() { override fun track(): DataFeed, SignedTransaction> { return DataFeed(txns.values.toList(), _updatesPublisher) } From e586822640ed544842787176c27d2f5f670d1b29 Mon Sep 17 00:00:00 2001 From: Andrzej Cichocki Date: Mon, 3 Jul 2017 17:54:30 +0100 Subject: [PATCH 39/97] Publish test-common. (#957) --- build.gradle | 2 +- test-common/build.gradle | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 62f0783ca8..9419b37017 100644 --- a/build.gradle +++ b/build.gradle @@ -248,7 +248,7 @@ bintrayConfig { projectUrl = 'https://github.com/corda/corda' gpgSign = true gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE') - publications = ['corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'cordform-common', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-node-schemas', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver'] + publications = ['corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'cordform-common', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-node-schemas', 'corda-test-common', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver'] license { name = 'Apache-2.0' url = 'https://www.apache.org/licenses/LICENSE-2.0' diff --git a/test-common/build.gradle b/test-common/build.gradle index 472fa597c3..3a955f1691 100644 --- a/test-common/build.gradle +++ b/test-common/build.gradle @@ -1 +1,9 @@ -// Nothing needed here currently. +apply plugin: 'net.corda.plugins.publish-utils' + +jar { + baseName 'corda-test-common' +} + +publish { + name = jar.baseName +} From 4e6ce97744dc5653727bc8d2fd62b3961eb49b4f Mon Sep 17 00:00:00 2001 From: Clinton Alexander Date: Wed, 28 Jun 2017 02:21:19 +0100 Subject: [PATCH 40/97] Corrected the group and version of cordform common to match the gradle plugins to avoid publishing issues. --- cordform-common/build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cordform-common/build.gradle b/cordform-common/build.gradle index c3c1676b23..340a4b6ec6 100644 --- a/cordform-common/build.gradle +++ b/cordform-common/build.gradle @@ -6,6 +6,10 @@ repositories { mavenCentral() } +// This tracks the gradle plugins version and not Corda +version gradle_plugins_version +group 'net.corda.plugins' + dependencies { // TypeSafe Config: for simple and human friendly config files. compile "com.typesafe:config:$typesafe_config_version" From 8e9591f3ab7bd5f1fd3a0dcde344ba5433101430 Mon Sep 17 00:00:00 2001 From: Clinton Alexander Date: Wed, 28 Jun 2017 02:23:15 +0100 Subject: [PATCH 41/97] Deleted publishing documentation because it is out of date and being rewritten on the Wiki. --- docs/source/publishing-corda.rst | 76 --------------------------- docs/source/release-process-index.rst | 1 - 2 files changed, 77 deletions(-) delete mode 100644 docs/source/publishing-corda.rst diff --git a/docs/source/publishing-corda.rst b/docs/source/publishing-corda.rst deleted file mode 100644 index 7b9f4b3c5e..0000000000 --- a/docs/source/publishing-corda.rst +++ /dev/null @@ -1,76 +0,0 @@ -Publishing Corda -================ - -Before Publishing ------------------ - -Before publishing you must make sure the version you plan to publish has a unique version number. Jcenter and Maven -Central will not allow overwriting old versions _unless_ the version is a snapshot. - -This guide assumes you are trying to publish to net.corda.*. Any other Maven coordinates require approval from Jcenter -and Maven Central. - -Publishing Locally ------------------- - -To publish the codebase locally to Maven Local you must run: - -.. code-block:: text - - gradlew install - -.. note:: This command is an alias for `publishToMavenLocal`. - -Publishing to Jcenter ---------------------- - -.. note:: The module you wish to publish must be linked to jcenter in Bintray. Only the founding account can do this. - -To publish to Jcenter you must first have the following; - -1. An account on Bintray in the R3 organisation -2. Our GPG key's passphrase for signing the binaries to publish - -Getting Setup -````````````` - -You must now set the following environment variables: - -* CORDA_BINTRAY_USER your Bintray username -* CORDA_BINTRAY_KEY to your Bintray API key (found at: https://bintray.com/profile/edit) -* CORDA_BINTRAY_GPG_PASSPHRASE to our GPG passphrase - -Publishing -`````````` - -Once you are setup you can upload all modules in a project with - -.. code-block:: text - - gradlew bintrayUpload - -Now login to Bintray and navigate to the corda repository, you will see a box stating you have published N files -and asking if you wish to publish. You can now publish to Bintray and Jcenter by clicking this button. - -.. warning:: Before publishing you should check that all of the files are uploaded and are signed. - -Within a minute your new version will be available to download and use. - -Publishing to Maven Central ---------------------------- - -To publish to Maven Central you need the following; - -1. An admin account on our Bintray R3 organisation -2. A published version in Bintray -3. An account with our Sonatype organisation (Maven Central's host) - -Publishing -`````````` - -1. Publish to Bintray -2. Navigate to the project you wish to publish -3. Click "Maven Central" -4. Enter your Sonatype credentials to publish a new version - -.. note:: The project you publish must be already published to Bintray and the project must be linked to Jcenter diff --git a/docs/source/release-process-index.rst b/docs/source/release-process-index.rst index ff8d39ea52..9e5276f786 100644 --- a/docs/source/release-process-index.rst +++ b/docs/source/release-process-index.rst @@ -6,5 +6,4 @@ Release process release-notes changelog - publishing-corda codestyle \ No newline at end of file From b4e7d7ca1b05f73c4aa4541921c0d4e8dd048f9a Mon Sep 17 00:00:00 2001 From: Clinton Alexander Date: Mon, 26 Jun 2017 18:07:56 +0100 Subject: [PATCH 42/97] Can now publish to artifactory. --- build.gradle | 17 ++++++++++++++++- client/jackson/build.gradle | 1 + client/jfx/build.gradle | 1 + client/mock/build.gradle | 1 + client/rpc/build.gradle | 1 + cordform-common/build.gradle | 1 + finance/build.gradle | 1 + node-api/build.gradle | 1 + node-schemas/build.gradle | 1 + node/build.gradle | 1 + node/capsule/build.gradle | 1 + test-utils/build.gradle | 1 + verifier/build.gradle | 1 + webserver/build.gradle | 1 + webserver/webcapsule/build.gradle | 1 + 15 files changed, 30 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 9419b37017..188541b59c 100644 --- a/build.gradle +++ b/build.gradle @@ -1,4 +1,3 @@ - buildscript { // For sharing constants between builds Properties constants = new Properties() @@ -75,6 +74,7 @@ plugins { // but the DSL has some restrictions e.g can't be used on the allprojects section. So we should revisit this if there are improvements in Gradle. // Version 1.0.2 of this plugin uses capsule:1.0.1 id "us.kirchmeier.capsule" version "1.0.2" + id "com.jfrog.artifactory" version "4.4.18" } ext { @@ -85,6 +85,7 @@ apply plugin: 'project-report' apply plugin: 'com.github.ben-manes.versions' apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'net.corda.plugins.cordformation' +apply plugin: 'maven-publish' // We need the following three lines even though they're inside an allprojects {} block below because otherwise // IntelliJ gets confused when importing the project and ends up erasing and recreating the .idea directory, along @@ -273,3 +274,17 @@ task buildCordappDependenciesZip(type: Zip) { from 'node/capsule/NOTICE' // CDDL notice duplicatesStrategy = DuplicatesStrategy.EXCLUDE } + +artifactory { + publish { + contextUrl = 'https://ci-artifactory.corda.r3cev.com/artifactory' + repository { + repoKey = 'corda-releases' + username = 'teamcity' + password = System.getenv('CORDA_ARTIFACTORY_PASSWORD') + } + defaults { + publications('corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'cordform-common', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-node-schemas', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver') + } + } +} diff --git a/client/jackson/build.gradle b/client/jackson/build.gradle index 818b742f5d..234f7e1ae0 100644 --- a/client/jackson/build.gradle +++ b/client/jackson/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'java' apply plugin: 'kotlin' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'com.jfrog.artifactory' dependencies { compile project(':core') diff --git a/client/jfx/build.gradle b/client/jfx/build.gradle index 69f39972c6..5d12e01f56 100644 --- a/client/jfx/build.gradle +++ b/client/jfx/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'kotlin' apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'com.jfrog.artifactory' description 'Corda client JavaFX modules' diff --git a/client/mock/build.gradle b/client/mock/build.gradle index 3b278fcbba..d709d4c911 100644 --- a/client/mock/build.gradle +++ b/client/mock/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'kotlin' apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'com.jfrog.artifactory' description 'Corda client mock modules' diff --git a/client/rpc/build.gradle b/client/rpc/build.gradle index 2a7a682cda..b2ab10dff4 100644 --- a/client/rpc/build.gradle +++ b/client/rpc/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'kotlin' apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'com.jfrog.artifactory' description 'Corda client RPC modules' diff --git a/cordform-common/build.gradle b/cordform-common/build.gradle index 340a4b6ec6..e15049fef1 100644 --- a/cordform-common/build.gradle +++ b/cordform-common/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'java' apply plugin: 'maven-publish' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'com.jfrog.artifactory' repositories { mavenCentral() diff --git a/finance/build.gradle b/finance/build.gradle index 6a554c33ec..3e4cae5813 100644 --- a/finance/build.gradle +++ b/finance/build.gradle @@ -5,6 +5,7 @@ apply plugin: 'kotlin-jpa' apply plugin: CanonicalizerPlugin apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'net.corda.plugins.quasar-utils' +apply plugin: 'com.jfrog.artifactory' description 'Corda finance modules' diff --git a/node-api/build.gradle b/node-api/build.gradle index 08ef79ae79..6bb83dcbf6 100644 --- a/node-api/build.gradle +++ b/node-api/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'kotlin' apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'com.jfrog.artifactory' description 'Corda node Artemis API' diff --git a/node-schemas/build.gradle b/node-schemas/build.gradle index 5026633777..b08f45d361 100644 --- a/node-schemas/build.gradle +++ b/node-schemas/build.gradle @@ -2,6 +2,7 @@ apply plugin: 'kotlin' apply plugin: 'kotlin-kapt' apply plugin: 'idea' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'com.jfrog.artifactory' description 'Corda node database schemas' diff --git a/node/build.gradle b/node/build.gradle index 1ca6b79f34..74d9bf04e7 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -5,6 +5,7 @@ apply plugin: 'kotlin-jpa' apply plugin: 'java' apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'com.jfrog.artifactory' description 'Corda node modules' diff --git a/node/capsule/build.gradle b/node/capsule/build.gradle index 9d60efab6c..fac1f538f5 100644 --- a/node/capsule/build.gradle +++ b/node/capsule/build.gradle @@ -4,6 +4,7 @@ */ apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'us.kirchmeier.capsule' +apply plugin: 'com.jfrog.artifactory' description 'Corda standalone node' diff --git a/test-utils/build.gradle b/test-utils/build.gradle index 6b0dc9b873..c0567edec9 100644 --- a/test-utils/build.gradle +++ b/test-utils/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'kotlin' apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'com.jfrog.artifactory' description 'Testing utilities for Corda' diff --git a/verifier/build.gradle b/verifier/build.gradle index db63013972..e0035ffa33 100644 --- a/verifier/build.gradle +++ b/verifier/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'kotlin' apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'com.jfrog.artifactory' description 'Corda verifier' diff --git a/webserver/build.gradle b/webserver/build.gradle index dc2e70e676..0868a6ec8a 100644 --- a/webserver/build.gradle +++ b/webserver/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'kotlin' apply plugin: 'java' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'com.jfrog.artifactory' description 'Corda node web server' diff --git a/webserver/webcapsule/build.gradle b/webserver/webcapsule/build.gradle index 08a8f0a9e1..9cfc1f8e0c 100644 --- a/webserver/webcapsule/build.gradle +++ b/webserver/webcapsule/build.gradle @@ -4,6 +4,7 @@ */ apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'us.kirchmeier.capsule' +apply plugin: 'com.jfrog.artifactory' description 'Corda node web server capsule' From f4a2f06bec7a1047f8ed806e587ba5e597f1afbc Mon Sep 17 00:00:00 2001 From: Clinton Alexander Date: Tue, 27 Jun 2017 22:31:08 +0100 Subject: [PATCH 43/97] Fixed gradle plugins build and cordform common publishing removed from core project. --- build.gradle | 2 +- cordform-common/build.gradle | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 188541b59c..4904203fc2 100644 --- a/build.gradle +++ b/build.gradle @@ -249,7 +249,7 @@ bintrayConfig { projectUrl = 'https://github.com/corda/corda' gpgSign = true gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE') - publications = ['corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'cordform-common', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-node-schemas', 'corda-test-common', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver'] + publications = ['corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-node-schemas', 'corda-test-common', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver'] license { name = 'Apache-2.0' url = 'https://www.apache.org/licenses/LICENSE-2.0' diff --git a/cordform-common/build.gradle b/cordform-common/build.gradle index e15049fef1..340a4b6ec6 100644 --- a/cordform-common/build.gradle +++ b/cordform-common/build.gradle @@ -1,7 +1,6 @@ apply plugin: 'java' apply plugin: 'maven-publish' apply plugin: 'net.corda.plugins.publish-utils' -apply plugin: 'com.jfrog.artifactory' repositories { mavenCentral() From a33d5dcd2f2c384d8128fc33117c6cb9cacdf0d2 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Tue, 4 Jul 2017 20:24:00 +0100 Subject: [PATCH 44/97] Add Tests to class name --- ...ndReturnEnvelope.kt => DeserializeAndReturnEnvelopeTests.kt} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename core/src/test/kotlin/net/corda/core/serialization/amqp/{DeserializeAndReturnEnvelope.kt => DeserializeAndReturnEnvelopeTests.kt} (97%) diff --git a/core/src/test/kotlin/net/corda/core/serialization/amqp/DeserializeAndReturnEnvelope.kt b/core/src/test/kotlin/net/corda/core/serialization/amqp/DeserializeAndReturnEnvelopeTests.kt similarity index 97% rename from core/src/test/kotlin/net/corda/core/serialization/amqp/DeserializeAndReturnEnvelope.kt rename to core/src/test/kotlin/net/corda/core/serialization/amqp/DeserializeAndReturnEnvelopeTests.kt index 16dad3f5a2..ca172680cf 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/amqp/DeserializeAndReturnEnvelope.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/amqp/DeserializeAndReturnEnvelopeTests.kt @@ -3,7 +3,7 @@ package net.corda.core.serialization.amqp import org.junit.Test import kotlin.test.* -class DeserializeAndReturnEnvelope { +class DeserializeAndReturnEnvelopeTests { fun testName() = Thread.currentThread().stackTrace[2].methodName inline fun classTestName(clazz: String) = "${this.javaClass.name}\$${testName()}\$$clazz" From 3ef5c39633c75e8587180c56964ad2f63df6e324 Mon Sep 17 00:00:00 2001 From: Matthew Nesbit Date: Tue, 4 Jul 2017 15:37:46 +0100 Subject: [PATCH 45/97] Put test classes into clear namespaces, so that they don't pollute the API. --- .../corda/core/contracts/DummyContractV2.kt | 60 --------------- .../core/contracts/DummyLinearContract.kt | 60 --------------- .../net/corda/core/contracts/DummyState.kt | 12 --- .../contracts/{ => testing}/DummyContract.kt | 7 +- .../core/contracts/testing/DummyContractV2.kt | 55 ++++++++++++++ .../contracts/testing/DummyLinearContract.kt | 54 ++++++++++++++ .../core/contracts/testing/DummyState.kt | 10 +++ .../net/corda/core/crypto/CryptoUtils.kt | 30 -------- .../corda/core/crypto/testing/DummyKeys.kt | 35 +++++++++ .../core/schemas/DummyLinearStateSchemaV2.kt | 29 -------- .../{ => testing}/DummyDealStateSchemaV1.kt | 4 +- .../{ => testing}/DummyLinearStateSchemaV1.kt | 4 +- .../testing/DummyLinearStateSchemaV2.kt | 24 ++++++ .../net/corda/core/utilities/TestConstants.kt | 1 + .../core/contracts/DummyContractV2Tests.kt | 3 +- .../contracts/TransactionGraphSearchTests.kt | 3 +- .../corda/core/contracts/TransactionTests.kt | 1 + .../contracts/clauses/VerifyClausesTests.kt | 6 +- .../core/flows/CollectSignaturesFlowTests.kt | 3 +- .../core/flows/ContractUpgradeFlowTest.kt | 2 + .../core/flows/ResolveTransactionsFlowTest.kt | 3 +- .../java/net/corda/docs/FlowCookbookJava.java | 2 + .../kotlin/net/corda/docs/FlowCookbook.kt | 2 + .../corda/contracts/JavaCommercialPaper.java | 41 ++++++---- .../corda/contracts/CommercialPaperLegacy.kt | 2 +- .../net/corda/contracts/DummyDealContract.kt | 5 +- .../kotlin/net/corda/contracts/asset/Cash.kt | 6 +- .../net/corda/contracts/asset/Obligation.kt | 2 +- .../corda/contracts/testing/VaultFiller.kt | 1 + .../net/corda/contracts/asset/CashTests.kt | 1 + .../corda/contracts/asset/ObligationTests.kt | 3 +- .../net/corda/contracts/testing/Generators.kt | 2 +- .../services/vault/schemas/VaultSchemaTest.kt | 7 +- .../node/services/BFTNotaryServiceTests.kt | 14 +++- .../node/services/RaftNotaryServiceTests.kt | 2 +- .../services/vault/VaultQueryJavaTests.java | 74 +++++++++++-------- .../corda/node/services/NotaryChangeTests.kt | 1 + .../database/HibernateConfigurationTest.kt | 4 +- .../database/RequeryConfigurationTest.kt | 4 +- .../services/events/ScheduledFlowTests.kt | 1 + .../persistence/DBTransactionStorageTests.kt | 2 +- .../statemachine/FlowFrameworkTests.kt | 7 +- .../transactions/NotaryServiceTests.kt | 2 +- .../ValidatingNotaryServiceTests.kt | 6 +- .../node/services/vault/VaultQueryTests.kt | 5 +- .../node/services/vault/VaultWithCashTest.kt | 4 +- .../notarydemo/flows/DummyIssueAndMove.kt | 4 +- .../main/kotlin/net/corda/testing/TestDSL.kt | 2 +- .../testing/TransactionDSLInterpreter.kt | 1 + .../net/corda/loadtest/tests/NotaryTest.kt | 2 +- .../net/corda/verifier/GeneratedLedger.kt | 6 +- 51 files changed, 341 insertions(+), 280 deletions(-) delete mode 100644 core/src/main/kotlin/net/corda/core/contracts/DummyContractV2.kt delete mode 100644 core/src/main/kotlin/net/corda/core/contracts/DummyLinearContract.kt delete mode 100644 core/src/main/kotlin/net/corda/core/contracts/DummyState.kt rename core/src/main/kotlin/net/corda/core/contracts/{ => testing}/DummyContract.kt (90%) create mode 100644 core/src/main/kotlin/net/corda/core/contracts/testing/DummyContractV2.kt create mode 100644 core/src/main/kotlin/net/corda/core/contracts/testing/DummyLinearContract.kt create mode 100644 core/src/main/kotlin/net/corda/core/contracts/testing/DummyState.kt create mode 100644 core/src/main/kotlin/net/corda/core/crypto/testing/DummyKeys.kt delete mode 100644 core/src/main/kotlin/net/corda/core/schemas/DummyLinearStateSchemaV2.kt rename core/src/main/kotlin/net/corda/core/schemas/{ => testing}/DummyDealStateSchemaV1.kt (76%) rename core/src/main/kotlin/net/corda/core/schemas/{ => testing}/DummyLinearStateSchemaV1.kt (92%) create mode 100644 core/src/main/kotlin/net/corda/core/schemas/testing/DummyLinearStateSchemaV2.kt diff --git a/core/src/main/kotlin/net/corda/core/contracts/DummyContractV2.kt b/core/src/main/kotlin/net/corda/core/contracts/DummyContractV2.kt deleted file mode 100644 index a0c4386236..0000000000 --- a/core/src/main/kotlin/net/corda/core/contracts/DummyContractV2.kt +++ /dev/null @@ -1,60 +0,0 @@ -package net.corda.core.contracts - -import net.corda.core.crypto.SecureHash -import net.corda.core.identity.AbstractParty -import net.corda.core.transactions.WireTransaction -import net.corda.flows.ContractUpgradeFlow - -// The dummy contract doesn't do anything useful. It exists for testing purposes. -val DUMMY_V2_PROGRAM_ID = DummyContractV2() - -/** - * Dummy contract state for testing of the upgrade process. - */ -// DOCSTART 1 -class DummyContractV2 : UpgradedContract { - override val legacyContract = DummyContract::class.java - - data class State(val magicNumber: Int = 0, val owners: List) : ContractState { - override val contract = DUMMY_V2_PROGRAM_ID - override val participants: List = owners - } - - interface Commands : CommandData { - class Create : TypeOnlyCommandData(), Commands - class Move : TypeOnlyCommandData(), Commands - } - - override fun upgrade(state: DummyContract.State): DummyContractV2.State { - return DummyContractV2.State(state.magicNumber, state.participants) - } - - override fun verify(tx: TransactionForContract) { - if (tx.commands.any { it.value is UpgradeCommand }) ContractUpgradeFlow.verify(tx) - // Other verifications. - } - - // The "empty contract" - override val legalContractReference: SecureHash = SecureHash.sha256("") - // DOCEND 1 - /** - * Generate an upgrade transaction from [DummyContract]. - * - * Note: This is a convenience helper method used for testing only. - * - * @return a pair of wire transaction, and a set of those who should sign the transaction for it to be valid. - */ - fun generateUpgradeFromV1(vararg states: StateAndRef): Pair> { - val notary = states.map { it.state.notary }.single() - require(states.isNotEmpty()) - - val signees: Set = states.flatMap { it.state.data.participants }.distinct().toSet() - return Pair(TransactionType.General.Builder(notary).apply { - states.forEach { - addInputState(it) - addOutputState(upgrade(it.state.data)) - addCommand(UpgradeCommand(DUMMY_V2_PROGRAM_ID.javaClass), signees.map { it.owningKey }.toList()) - } - }.toWireTransaction(), signees) - } -} diff --git a/core/src/main/kotlin/net/corda/core/contracts/DummyLinearContract.kt b/core/src/main/kotlin/net/corda/core/contracts/DummyLinearContract.kt deleted file mode 100644 index a2bffbe78d..0000000000 --- a/core/src/main/kotlin/net/corda/core/contracts/DummyLinearContract.kt +++ /dev/null @@ -1,60 +0,0 @@ -package net.corda.core.contracts - -import net.corda.core.contracts.clauses.Clause -import net.corda.core.contracts.clauses.FilterOn -import net.corda.core.contracts.clauses.verifyClause -import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.containsAny -import net.corda.core.identity.AbstractParty -import net.corda.core.schemas.* -import java.security.PublicKey -import java.time.Instant -import java.time.LocalDateTime -import java.time.ZoneOffset - -class DummyLinearContract : Contract { - override val legalContractReference: SecureHash = SecureHash.sha256("Test") - - val clause: Clause = LinearState.ClauseVerifier() - override fun verify(tx: TransactionForContract) = verifyClause(tx, - FilterOn(clause, { states -> states.filterIsInstance() }), - emptyList()) - - data class State( - override val linearId: UniqueIdentifier = UniqueIdentifier(), - override val contract: Contract = DummyLinearContract(), - override val participants: List = listOf(), - val linearString: String = "ABC", - val linearNumber: Long = 123L, - val linearTimestamp: Instant = LocalDateTime.now().toInstant(ZoneOffset.UTC), - val linearBoolean: Boolean = true, - val nonce: SecureHash = SecureHash.randomSHA256()) : LinearState, QueryableState { - - override fun isRelevant(ourKeys: Set): Boolean { - return participants.any { it.owningKey.containsAny(ourKeys) } - } - - override fun supportedSchemas(): Iterable = listOf(DummyLinearStateSchemaV1, DummyLinearStateSchemaV2) - - override fun generateMappedObject(schema: MappedSchema): PersistentState { - return when (schema) { - is DummyLinearStateSchemaV1 -> DummyLinearStateSchemaV1.PersistentDummyLinearState( - externalId = linearId.externalId, - uuid = linearId.id, - linearString = linearString, - linearNumber = linearNumber, - linearTimestamp = linearTimestamp, - linearBoolean = linearBoolean - ) - is DummyLinearStateSchemaV2 -> DummyLinearStateSchemaV2.PersistentDummyLinearState( - uid = linearId, - linearString = linearString, - linearNumber = linearNumber, - linearTimestamp = linearTimestamp, - linearBoolean = linearBoolean - ) - else -> throw IllegalArgumentException("Unrecognised schema $schema") - } - } - } -} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/contracts/DummyState.kt b/core/src/main/kotlin/net/corda/core/contracts/DummyState.kt deleted file mode 100644 index 1498b8c379..0000000000 --- a/core/src/main/kotlin/net/corda/core/contracts/DummyState.kt +++ /dev/null @@ -1,12 +0,0 @@ -package net.corda.core.contracts - -import net.corda.core.identity.AbstractParty - -/** - * Dummy state for use in testing. Not part of any contract, not even the [DummyContract]. - */ -data class DummyState(val magicNumber: Int = 0) : ContractState { - override val contract = DUMMY_PROGRAM_ID - override val participants: List - get() = emptyList() -} diff --git a/core/src/main/kotlin/net/corda/core/contracts/DummyContract.kt b/core/src/main/kotlin/net/corda/core/contracts/testing/DummyContract.kt similarity index 90% rename from core/src/main/kotlin/net/corda/core/contracts/DummyContract.kt rename to core/src/main/kotlin/net/corda/core/contracts/testing/DummyContract.kt index 31e85f859c..5edc0d4f19 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/DummyContract.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/testing/DummyContract.kt @@ -1,5 +1,6 @@ -package net.corda.core.contracts +package net.corda.core.contracts.testing +import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party @@ -55,8 +56,8 @@ data class DummyContract(override val legalContractReference: SecureHash = Secur } } - fun move(prior: StateAndRef, newOwner: AbstractParty) = move(listOf(prior), newOwner) - fun move(priors: List>, newOwner: AbstractParty): TransactionBuilder { + fun move(prior: StateAndRef, newOwner: AbstractParty) = move(listOf(prior), newOwner) + fun move(priors: List>, newOwner: AbstractParty): TransactionBuilder { require(priors.isNotEmpty()) val priorState = priors[0].state.data val (cmd, state) = priorState.withNewOwner(newOwner) diff --git a/core/src/main/kotlin/net/corda/core/contracts/testing/DummyContractV2.kt b/core/src/main/kotlin/net/corda/core/contracts/testing/DummyContractV2.kt new file mode 100644 index 0000000000..5e6be3631e --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/contracts/testing/DummyContractV2.kt @@ -0,0 +1,55 @@ +package net.corda.core.contracts.testing + +// The dummy contract doesn't do anything useful. It exists for testing purposes. +val DUMMY_V2_PROGRAM_ID = net.corda.core.contracts.testing.DummyContractV2() + +/** + * Dummy contract state for testing of the upgrade process. + */ +// DOCSTART 1 +class DummyContractV2 : net.corda.core.contracts.UpgradedContract { + override val legacyContract = DummyContract::class.java + + data class State(val magicNumber: Int = 0, val owners: List) : net.corda.core.contracts.ContractState { + override val contract = net.corda.core.contracts.testing.DUMMY_V2_PROGRAM_ID + override val participants: List = owners + } + + interface Commands : net.corda.core.contracts.CommandData { + class Create : net.corda.core.contracts.TypeOnlyCommandData(), net.corda.core.contracts.testing.DummyContractV2.Commands + class Move : net.corda.core.contracts.TypeOnlyCommandData(), net.corda.core.contracts.testing.DummyContractV2.Commands + } + + override fun upgrade(state: DummyContract.State): net.corda.core.contracts.testing.DummyContractV2.State { + return net.corda.core.contracts.testing.DummyContractV2.State(state.magicNumber, state.participants) + } + + override fun verify(tx: net.corda.core.contracts.TransactionForContract) { + if (tx.commands.any { it.value is net.corda.core.contracts.UpgradeCommand }) net.corda.flows.ContractUpgradeFlow.Companion.verify(tx) + // Other verifications. + } + + // The "empty contract" + override val legalContractReference: net.corda.core.crypto.SecureHash = net.corda.core.crypto.SecureHash.Companion.sha256("") + // DOCEND 1 + /** + * Generate an upgrade transaction from [DummyContract]. + * + * Note: This is a convenience helper method used for testing only. + * + * @return a pair of wire transaction, and a set of those who should sign the transaction for it to be valid. + */ + fun generateUpgradeFromV1(vararg states: net.corda.core.contracts.StateAndRef): Pair> { + val notary = states.map { it.state.notary }.single() + require(states.isNotEmpty()) + + val signees: Set = states.flatMap { it.state.data.participants }.distinct().toSet() + return Pair(net.corda.core.contracts.TransactionType.General.Builder(notary).apply { + states.forEach { + addInputState(it) + addOutputState(upgrade(it.state.data)) + addCommand(net.corda.core.contracts.UpgradeCommand(DUMMY_V2_PROGRAM_ID.javaClass), signees.map { it.owningKey }.toList()) + } + }.toWireTransaction(), signees) + } +} diff --git a/core/src/main/kotlin/net/corda/core/contracts/testing/DummyLinearContract.kt b/core/src/main/kotlin/net/corda/core/contracts/testing/DummyLinearContract.kt new file mode 100644 index 0000000000..2723de3a61 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/contracts/testing/DummyLinearContract.kt @@ -0,0 +1,54 @@ +package net.corda.core.contracts.testing + +import net.corda.core.contracts.CommandData +import net.corda.core.contracts.clauses.FilterOn +import net.corda.core.crypto.containsAny +import net.corda.core.schemas.testing.DummyLinearStateSchemaV1 +import net.corda.core.schemas.testing.DummyLinearStateSchemaV2 + +class DummyLinearContract : net.corda.core.contracts.Contract { + override val legalContractReference: net.corda.core.crypto.SecureHash = net.corda.core.crypto.SecureHash.Companion.sha256("Test") + + val clause: net.corda.core.contracts.clauses.Clause = net.corda.core.contracts.LinearState.ClauseVerifier() + override fun verify(tx: net.corda.core.contracts.TransactionForContract) = net.corda.core.contracts.clauses.verifyClause(tx, + FilterOn(clause, { states -> states.filterIsInstance() }), + emptyList()) + + data class State( + override val linearId: net.corda.core.contracts.UniqueIdentifier = net.corda.core.contracts.UniqueIdentifier(), + override val contract: net.corda.core.contracts.Contract = net.corda.core.contracts.testing.DummyLinearContract(), + override val participants: List = listOf(), + val linearString: String = "ABC", + val linearNumber: Long = 123L, + val linearTimestamp: java.time.Instant = java.time.LocalDateTime.now().toInstant(java.time.ZoneOffset.UTC), + val linearBoolean: Boolean = true, + val nonce: net.corda.core.crypto.SecureHash = net.corda.core.crypto.SecureHash.Companion.randomSHA256()) : net.corda.core.contracts.LinearState, net.corda.core.schemas.QueryableState { + + override fun isRelevant(ourKeys: Set): Boolean { + return participants.any { it.owningKey.containsAny(ourKeys) } + } + + override fun supportedSchemas(): Iterable = listOf(DummyLinearStateSchemaV1, DummyLinearStateSchemaV2) + + override fun generateMappedObject(schema: net.corda.core.schemas.MappedSchema): net.corda.core.schemas.PersistentState { + return when (schema) { + is DummyLinearStateSchemaV1 -> DummyLinearStateSchemaV1.PersistentDummyLinearState( + externalId = linearId.externalId, + uuid = linearId.id, + linearString = linearString, + linearNumber = linearNumber, + linearTimestamp = linearTimestamp, + linearBoolean = linearBoolean + ) + is DummyLinearStateSchemaV2 -> DummyLinearStateSchemaV2.PersistentDummyLinearState( + uid = linearId, + linearString = linearString, + linearNumber = linearNumber, + linearTimestamp = linearTimestamp, + linearBoolean = linearBoolean + ) + else -> throw IllegalArgumentException("Unrecognised schema $schema") + } + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/contracts/testing/DummyState.kt b/core/src/main/kotlin/net/corda/core/contracts/testing/DummyState.kt new file mode 100644 index 0000000000..6c852bb33d --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/contracts/testing/DummyState.kt @@ -0,0 +1,10 @@ +package net.corda.core.contracts.testing + +/** + * Dummy state for use in testing. Not part of any contract, not even the [DummyContract]. + */ +data class DummyState(val magicNumber: Int = 0) : net.corda.core.contracts.ContractState { + override val contract = DUMMY_PROGRAM_ID + override val participants: List + get() = emptyList() +} diff --git a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt index 02d0c11627..2094a526c7 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt @@ -2,41 +2,11 @@ package net.corda.core.crypto -import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party -import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.OpaqueBytes import java.math.BigInteger import java.security.* -@CordaSerializable -object NullPublicKey : PublicKey, Comparable { - override fun getAlgorithm() = "NULL" - override fun getEncoded() = byteArrayOf(0) - override fun getFormat() = "NULL" - override fun compareTo(other: PublicKey): Int = if (other == NullPublicKey) 0 else -1 - override fun toString() = "NULL_KEY" -} - -val NULL_PARTY = AnonymousParty(NullPublicKey) - -// TODO: Clean up this duplication between Null and Dummy public key -@CordaSerializable -@Deprecated("Has encoding format problems, consider entropyToKeyPair() instead") -class DummyPublicKey(val s: String) : PublicKey, Comparable { - override fun getAlgorithm() = "DUMMY" - override fun getEncoded() = s.toByteArray() - override fun getFormat() = "ASN.1" - override fun compareTo(other: PublicKey): Int = BigInteger(encoded).compareTo(BigInteger(other.encoded)) - override fun equals(other: Any?) = other is DummyPublicKey && other.s == s - override fun hashCode(): Int = s.hashCode() - override fun toString() = "PUBKEY[$s]" -} - -/** A signature with a key and value of zero. Useful when you want a signature object that you know won't ever be used. */ -@CordaSerializable -object NullSignature : DigitalSignature.WithKey(NullPublicKey, ByteArray(32)) - /** * Utility to simplify the act of signing a byte array. * @param bytesToSign the data/message to be signed in [ByteArray] form (usually the Merkle root). diff --git a/core/src/main/kotlin/net/corda/core/crypto/testing/DummyKeys.kt b/core/src/main/kotlin/net/corda/core/crypto/testing/DummyKeys.kt new file mode 100644 index 0000000000..8b699ef38d --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/crypto/testing/DummyKeys.kt @@ -0,0 +1,35 @@ +package net.corda.core.crypto.testing + +import net.corda.core.crypto.DigitalSignature +import net.corda.core.identity.AnonymousParty +import net.corda.core.serialization.CordaSerializable +import java.math.BigInteger +import java.security.PublicKey + +@CordaSerializable +object NullPublicKey : PublicKey, Comparable { + override fun getAlgorithm() = "NULL" + override fun getEncoded() = byteArrayOf(0) + override fun getFormat() = "NULL" + override fun compareTo(other: PublicKey): Int = if (other == NullPublicKey) 0 else -1 + override fun toString() = "NULL_KEY" +} + +val NULL_PARTY = AnonymousParty(NullPublicKey) + +// TODO: Clean up this duplication between Null and Dummy public key +@CordaSerializable +@Deprecated("Has encoding format problems, consider entropyToKeyPair() instead") +class DummyPublicKey(val s: String) : PublicKey, Comparable { + override fun getAlgorithm() = "DUMMY" + override fun getEncoded() = s.toByteArray() + override fun getFormat() = "ASN.1" + override fun compareTo(other: PublicKey): Int = BigInteger(encoded).compareTo(BigInteger(other.encoded)) + override fun equals(other: Any?) = other is DummyPublicKey && other.s == s + override fun hashCode(): Int = s.hashCode() + override fun toString() = "PUBKEY[$s]" +} + +/** A signature with a key and value of zero. Useful when you want a signature object that you know won't ever be used. */ +@CordaSerializable +object NullSignature : DigitalSignature.WithKey(NullPublicKey, ByteArray(32)) \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/schemas/DummyLinearStateSchemaV2.kt b/core/src/main/kotlin/net/corda/core/schemas/DummyLinearStateSchemaV2.kt deleted file mode 100644 index 247c061a0f..0000000000 --- a/core/src/main/kotlin/net/corda/core/schemas/DummyLinearStateSchemaV2.kt +++ /dev/null @@ -1,29 +0,0 @@ -package net.corda.core.schemas - -import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1 -import javax.persistence.Column -import javax.persistence.Entity -import javax.persistence.Table - -/** - * Second version of a cash contract ORM schema that extends the common - * [VaultLinearState] abstract schema - */ -object DummyLinearStateSchemaV2 : net.corda.core.schemas.MappedSchema(schemaFamily = DummyLinearStateSchema.javaClass, version = 2, - mappedTypes = listOf(PersistentDummyLinearState::class.java)) { - @Entity - @Table(name = "dummy_linear_states_v2") - class PersistentDummyLinearState( - @Column(name = "linear_string") var linearString: String, - - @Column(name = "linear_number") var linearNumber: Long, - - @Column(name = "linear_timestamp") var linearTimestamp: java.time.Instant, - - @Column(name = "linear_boolean") var linearBoolean: Boolean, - - /** parent attributes */ - @Transient - val uid: net.corda.core.contracts.UniqueIdentifier - ) : CommonSchemaV1.LinearState(uid = uid) -} diff --git a/core/src/main/kotlin/net/corda/core/schemas/DummyDealStateSchemaV1.kt b/core/src/main/kotlin/net/corda/core/schemas/testing/DummyDealStateSchemaV1.kt similarity index 76% rename from core/src/main/kotlin/net/corda/core/schemas/DummyDealStateSchemaV1.kt rename to core/src/main/kotlin/net/corda/core/schemas/testing/DummyDealStateSchemaV1.kt index e26382143f..4b7a2a5b68 100644 --- a/core/src/main/kotlin/net/corda/core/schemas/DummyDealStateSchemaV1.kt +++ b/core/src/main/kotlin/net/corda/core/schemas/testing/DummyDealStateSchemaV1.kt @@ -1,4 +1,4 @@ -package net.corda.core.schemas +package net.corda.core.schemas.testing /** * An object used to fully qualify the [DummyDealStateSchema] family name (i.e. independent of version). @@ -9,7 +9,7 @@ object DummyDealStateSchema * First version of a cash contract ORM schema that maps all fields of the [DummyDealState] contract state as it stood * at the time of writing. */ -object DummyDealStateSchemaV1 : net.corda.core.schemas.MappedSchema(schemaFamily = net.corda.core.schemas.DummyDealStateSchema.javaClass, version = 1, mappedTypes = listOf(net.corda.core.schemas.DummyDealStateSchemaV1.PersistentDummyDealState::class.java)) { +object DummyDealStateSchemaV1 : net.corda.core.schemas.MappedSchema(schemaFamily = net.corda.core.schemas.testing.DummyDealStateSchema.javaClass, version = 1, mappedTypes = listOf(net.corda.core.schemas.testing.DummyDealStateSchemaV1.PersistentDummyDealState::class.java)) { @javax.persistence.Entity @javax.persistence.Table(name = "dummy_deal_states") class PersistentDummyDealState( diff --git a/core/src/main/kotlin/net/corda/core/schemas/DummyLinearStateSchemaV1.kt b/core/src/main/kotlin/net/corda/core/schemas/testing/DummyLinearStateSchemaV1.kt similarity index 92% rename from core/src/main/kotlin/net/corda/core/schemas/DummyLinearStateSchemaV1.kt rename to core/src/main/kotlin/net/corda/core/schemas/testing/DummyLinearStateSchemaV1.kt index a0c3c82649..3a0b8e3a66 100644 --- a/core/src/main/kotlin/net/corda/core/schemas/DummyLinearStateSchemaV1.kt +++ b/core/src/main/kotlin/net/corda/core/schemas/testing/DummyLinearStateSchemaV1.kt @@ -1,5 +1,7 @@ -package net.corda.core.schemas +package net.corda.core.schemas.testing +import net.corda.core.schemas.MappedSchema +import net.corda.core.schemas.PersistentState import java.time.Instant import java.util.* import javax.persistence.Column diff --git a/core/src/main/kotlin/net/corda/core/schemas/testing/DummyLinearStateSchemaV2.kt b/core/src/main/kotlin/net/corda/core/schemas/testing/DummyLinearStateSchemaV2.kt new file mode 100644 index 0000000000..ec2f36d86c --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/schemas/testing/DummyLinearStateSchemaV2.kt @@ -0,0 +1,24 @@ +package net.corda.core.schemas.testing + +/** + * Second version of a cash contract ORM schema that extends the common + * [VaultLinearState] abstract schema + */ +object DummyLinearStateSchemaV2 : net.corda.core.schemas.MappedSchema(schemaFamily = DummyLinearStateSchema.javaClass, version = 2, + mappedTypes = listOf(net.corda.core.schemas.testing.DummyLinearStateSchemaV2.PersistentDummyLinearState::class.java)) { + @javax.persistence.Entity + @javax.persistence.Table(name = "dummy_linear_states_v2") + class PersistentDummyLinearState( + @javax.persistence.Column(name = "linear_string") var linearString: String, + + @javax.persistence.Column(name = "linear_number") var linearNumber: Long, + + @javax.persistence.Column(name = "linear_timestamp") var linearTimestamp: java.time.Instant, + + @javax.persistence.Column(name = "linear_boolean") var linearBoolean: Boolean, + + /** parent attributes */ + @Transient + val uid: net.corda.core.contracts.UniqueIdentifier + ) : net.corda.node.services.vault.schemas.jpa.CommonSchemaV1.LinearState(uid = uid) +} 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 6861fa7fc0..b42a903493 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/TestConstants.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/TestConstants.kt @@ -3,6 +3,7 @@ package net.corda.core.utilities import net.corda.core.crypto.* +import net.corda.core.crypto.testing.DummyPublicKey import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import org.bouncycastle.asn1.x500.X500Name diff --git a/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt b/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt index dfe175ded2..802d275bd5 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt @@ -1,9 +1,10 @@ package net.corda.core.contracts +import net.corda.core.contracts.testing.DummyContract +import net.corda.core.contracts.testing.DummyContractV2 import net.corda.core.crypto.SecureHash import net.corda.core.utilities.ALICE import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.testing.ALICE_PUBKEY import org.junit.Test import kotlin.test.assertEquals import kotlin.test.assertTrue diff --git a/core/src/test/kotlin/net/corda/core/contracts/TransactionGraphSearchTests.kt b/core/src/test/kotlin/net/corda/core/contracts/TransactionGraphSearchTests.kt index c4557fd75d..aaa09b7ef5 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/TransactionGraphSearchTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/TransactionGraphSearchTests.kt @@ -1,5 +1,7 @@ package net.corda.core.contracts +import net.corda.core.contracts.testing.DummyContract +import net.corda.core.contracts.testing.DummyState import net.corda.core.crypto.newSecureRandom import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction @@ -10,7 +12,6 @@ import net.corda.testing.MEGA_CORP_PUBKEY import net.corda.testing.node.MockServices import net.corda.testing.node.MockTransactionStorage import org.junit.Test -import java.security.KeyPair import kotlin.test.assertEquals class TransactionGraphSearchTests { diff --git a/core/src/test/kotlin/net/corda/core/contracts/TransactionTests.kt b/core/src/test/kotlin/net/corda/core/contracts/TransactionTests.kt index 61750d7619..c42ffb4a28 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/TransactionTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/TransactionTests.kt @@ -1,6 +1,7 @@ package net.corda.core.contracts import net.corda.contracts.asset.DUMMY_CASH_ISSUER_KEY +import net.corda.core.contracts.testing.DummyContract import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.SecureHash import net.corda.core.crypto.generateKeyPair diff --git a/core/src/test/kotlin/net/corda/core/contracts/clauses/VerifyClausesTests.kt b/core/src/test/kotlin/net/corda/core/contracts/clauses/VerifyClausesTests.kt index 5a7aba240c..076d7e2854 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/clauses/VerifyClausesTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/clauses/VerifyClausesTests.kt @@ -1,6 +1,10 @@ package net.corda.core.contracts.clauses -import net.corda.core.contracts.* +import net.corda.core.contracts.AuthenticatedObject +import net.corda.core.contracts.CommandData +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.TransactionForContract +import net.corda.core.contracts.testing.DummyContract import net.corda.core.crypto.SecureHash import org.junit.Test import kotlin.test.assertFailsWith diff --git a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt index 91672d3133..fdc472ab1e 100644 --- a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt @@ -2,9 +2,9 @@ package net.corda.core.flows import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.Command -import net.corda.core.contracts.DummyContract import net.corda.core.contracts.TransactionType import net.corda.core.contracts.requireThat +import net.corda.core.contracts.testing.DummyContract import net.corda.core.getOrThrow import net.corda.core.identity.Party import net.corda.core.transactions.SignedTransaction @@ -13,7 +13,6 @@ import net.corda.flows.CollectSignaturesFlow import net.corda.flows.FinalityFlow import net.corda.flows.SignTransactionFlow import net.corda.testing.MINI_CORP_KEY -import net.corda.testing.MINI_CORP_PUBKEY import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockServices import org.junit.After diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt index 38289537f1..d8661ac41c 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -3,6 +3,8 @@ package net.corda.core.flows import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.asset.Cash import net.corda.core.contracts.* +import net.corda.core.contracts.testing.DummyContract +import net.corda.core.contracts.testing.DummyContractV2 import net.corda.core.crypto.SecureHash import net.corda.core.getOrThrow import net.corda.core.identity.AbstractParty diff --git a/core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt index 8dccc62f9a..852c14b4fb 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt @@ -1,7 +1,6 @@ package net.corda.core.flows -import net.corda.core.contracts.DummyContract -import net.corda.core.crypto.NullSignature +import net.corda.core.contracts.testing.DummyContract import net.corda.core.crypto.SecureHash import net.corda.core.getOrThrow import net.corda.core.identity.Party diff --git a/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java b/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java index b50156b057..6392a4e133 100644 --- a/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java +++ b/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java @@ -7,6 +7,8 @@ import net.corda.contracts.asset.Cash; import net.corda.core.contracts.*; import net.corda.core.contracts.TransactionType.General; import net.corda.core.contracts.TransactionType.NotaryChange; +import net.corda.core.contracts.testing.DummyContract; +import net.corda.core.contracts.testing.DummyState; import net.corda.core.crypto.SecureHash; import net.corda.core.flows.*; import net.corda.core.identity.Party; diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt index e906651f32..fa25e494e8 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt @@ -5,6 +5,8 @@ import net.corda.contracts.asset.Cash import net.corda.core.contracts.* import net.corda.core.contracts.TransactionType.General import net.corda.core.contracts.TransactionType.NotaryChange +import net.corda.core.contracts.testing.DummyContract +import net.corda.core.contracts.testing.DummyState import net.corda.core.crypto.SecureHash import net.corda.core.flows.* import net.corda.core.identity.Party diff --git a/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java b/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java index bc1d505ff9..07b071c98e 100644 --- a/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java +++ b/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java @@ -1,27 +1,36 @@ package net.corda.contracts; -import co.paralleluniverse.fibers.*; -import com.google.common.collect.*; -import kotlin.*; -import net.corda.contracts.asset.*; +import co.paralleluniverse.fibers.Suspendable; +import com.google.common.collect.ImmutableList; +import kotlin.Pair; +import kotlin.Unit; +import net.corda.contracts.asset.CashKt; import net.corda.core.contracts.*; -import net.corda.core.contracts.Contract; -import net.corda.core.contracts.TransactionForContract.*; -import net.corda.core.contracts.clauses.*; -import net.corda.core.crypto.*; +import net.corda.core.contracts.TransactionForContract.InOutGroup; +import net.corda.core.contracts.clauses.AnyOf; +import net.corda.core.contracts.clauses.Clause; +import net.corda.core.contracts.clauses.ClauseVerifier; +import net.corda.core.contracts.clauses.GroupClauseVerifier; +import net.corda.core.crypto.SecureHash; +import net.corda.core.crypto.testing.NullPublicKey; import net.corda.core.identity.AbstractParty; import net.corda.core.identity.AnonymousParty; import net.corda.core.identity.Party; -import net.corda.core.node.services.*; -import net.corda.core.transactions.*; -import org.jetbrains.annotations.*; +import net.corda.core.node.services.VaultService; +import net.corda.core.transactions.TransactionBuilder; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; -import java.time.*; -import java.util.*; -import java.util.stream.*; +import java.time.Instant; +import java.util.Collections; +import java.util.Currency; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; -import static kotlin.collections.CollectionsKt.*; -import static net.corda.core.contracts.ContractsDSL.*; +import static kotlin.collections.CollectionsKt.single; +import static net.corda.core.contracts.ContractsDSL.requireSingleCommand; +import static net.corda.core.contracts.ContractsDSL.requireThat; /** diff --git a/finance/src/main/kotlin/net/corda/contracts/CommercialPaperLegacy.kt b/finance/src/main/kotlin/net/corda/contracts/CommercialPaperLegacy.kt index c8beea5c0e..0c3811556b 100644 --- a/finance/src/main/kotlin/net/corda/contracts/CommercialPaperLegacy.kt +++ b/finance/src/main/kotlin/net/corda/contracts/CommercialPaperLegacy.kt @@ -3,8 +3,8 @@ package net.corda.contracts import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.asset.sumCashBy import net.corda.core.contracts.* -import net.corda.core.crypto.NULL_PARTY import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.testing.NULL_PARTY import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.node.services.VaultService diff --git a/finance/src/main/kotlin/net/corda/contracts/DummyDealContract.kt b/finance/src/main/kotlin/net/corda/contracts/DummyDealContract.kt index 04ca026671..39041fbef5 100644 --- a/finance/src/main/kotlin/net/corda/contracts/DummyDealContract.kt +++ b/finance/src/main/kotlin/net/corda/contracts/DummyDealContract.kt @@ -3,13 +3,14 @@ package net.corda.contracts import net.corda.core.contracts.Contract import net.corda.core.contracts.TransactionForContract import net.corda.core.contracts.UniqueIdentifier -import net.corda.core.crypto.* +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.containsAny import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party -import net.corda.core.schemas.DummyDealStateSchemaV1 import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState import net.corda.core.schemas.QueryableState +import net.corda.core.schemas.testing.DummyDealStateSchemaV1 import net.corda.core.transactions.TransactionBuilder import java.security.PublicKey diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt b/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt index 62458b59da..f47fb51ec1 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt @@ -8,7 +8,11 @@ import net.corda.core.contracts.clauses.AllOf import net.corda.core.contracts.clauses.FirstOf import net.corda.core.contracts.clauses.GroupClauseVerifier import net.corda.core.contracts.clauses.verifyClause -import net.corda.core.crypto.* +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.entropyToKeyPair +import net.corda.core.crypto.newSecureRandom +import net.corda.core.crypto.testing.NULL_PARTY +import net.corda.core.crypto.toBase58String import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.schemas.MappedSchema diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt b/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt index 1af1959760..cfd6abdab3 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt @@ -8,9 +8,9 @@ import net.corda.contracts.asset.Obligation.Lifecycle.NORMAL import net.corda.contracts.clause.* import net.corda.core.contracts.* import net.corda.core.contracts.clauses.* -import net.corda.core.crypto.NULL_PARTY import net.corda.core.crypto.SecureHash import net.corda.core.crypto.entropyToKeyPair +import net.corda.core.crypto.testing.NULL_PARTY import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party diff --git a/finance/src/main/kotlin/net/corda/contracts/testing/VaultFiller.kt b/finance/src/main/kotlin/net/corda/contracts/testing/VaultFiller.kt index b7fdd8c06f..923a545d52 100644 --- a/finance/src/main/kotlin/net/corda/contracts/testing/VaultFiller.kt +++ b/finance/src/main/kotlin/net/corda/contracts/testing/VaultFiller.kt @@ -7,6 +7,7 @@ import net.corda.contracts.DealState import net.corda.contracts.DummyDealContract import net.corda.contracts.asset.* import net.corda.core.contracts.* +import net.corda.core.contracts.testing.DummyLinearContract import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party diff --git a/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt b/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt index 6dff6c60e2..27f355d1a7 100644 --- a/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt @@ -2,6 +2,7 @@ package net.corda.contracts.asset import net.corda.contracts.testing.fillWithSomeTestCash import net.corda.core.contracts.* +import net.corda.core.contracts.testing.DummyState import net.corda.core.crypto.SecureHash import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.AbstractParty diff --git a/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt b/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt index 7b9d18161f..8cb3d8f677 100644 --- a/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt @@ -4,8 +4,9 @@ import net.corda.contracts.Commodity import net.corda.contracts.NetType import net.corda.contracts.asset.Obligation.Lifecycle import net.corda.core.contracts.* -import net.corda.core.crypto.NULL_PARTY +import net.corda.core.contracts.testing.DummyState import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.testing.NULL_PARTY import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.serialization.OpaqueBytes diff --git a/finance/src/test/kotlin/net/corda/contracts/testing/Generators.kt b/finance/src/test/kotlin/net/corda/contracts/testing/Generators.kt index fe65daa626..1ca4c5d998 100644 --- a/finance/src/test/kotlin/net/corda/contracts/testing/Generators.kt +++ b/finance/src/test/kotlin/net/corda/contracts/testing/Generators.kt @@ -9,7 +9,7 @@ import net.corda.core.contracts.Command import net.corda.core.contracts.CommandData import net.corda.core.contracts.ContractState import net.corda.core.contracts.TransactionType -import net.corda.core.crypto.NullSignature +import net.corda.core.crypto.testing.NullSignature import net.corda.core.identity.AnonymousParty import net.corda.core.testing.* import net.corda.core.transactions.SignedTransaction diff --git a/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt b/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt index b54cb0d961..5d552f6a9e 100644 --- a/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt +++ b/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt @@ -8,7 +8,7 @@ import io.requery.rx.KotlinRxEntityStore import io.requery.sql.* import io.requery.sql.platform.Generic import net.corda.core.contracts.* -import net.corda.core.contracts.TimeWindow +import net.corda.core.contracts.testing.DummyContract import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.SecureHash import net.corda.core.crypto.generateKeyPair @@ -21,7 +21,10 @@ import net.corda.core.schemas.requery.converters.VaultStateStatusConverter import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.transactions.LedgerTransaction -import net.corda.core.utilities.* +import net.corda.core.utilities.ALICE +import net.corda.core.utilities.BOB +import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.core.utilities.DUMMY_NOTARY_KEY import net.corda.node.services.vault.schemas.requery.* import org.h2.jdbcx.JdbcDataSource import org.junit.After diff --git a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt index 35527d3f78..d0aeb6547b 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt @@ -2,10 +2,15 @@ package net.corda.node.services import com.google.common.net.HostAndPort import com.nhaarman.mockito_kotlin.whenever -import net.corda.core.* -import net.corda.core.contracts.* +import net.corda.core.ErrorOr +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TransactionType +import net.corda.core.contracts.testing.DummyContract import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.SecureHash +import net.corda.core.div +import net.corda.core.getOrThrow import net.corda.core.identity.Party import net.corda.core.node.services.ServiceInfo import net.corda.flows.NotaryError @@ -20,11 +25,12 @@ import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.node.utilities.transaction import net.corda.testing.node.MockNetwork import org.bouncycastle.asn1.x500.X500Name -import org.junit.Ignore import org.junit.After +import org.junit.Ignore import org.junit.Test import java.nio.file.Files -import kotlin.test.* +import kotlin.test.assertEquals +import kotlin.test.assertTrue class BFTNotaryServiceTests { companion object { diff --git a/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt index c170de0647..6fe61fb40d 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt @@ -1,10 +1,10 @@ package net.corda.node.services import com.google.common.util.concurrent.Futures -import net.corda.core.contracts.DummyContract import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef import net.corda.core.contracts.TransactionType +import net.corda.core.contracts.testing.DummyContract import net.corda.core.getOrThrow import net.corda.core.identity.Party import net.corda.core.map diff --git a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java index 305a14407a..38deb5639a 100644 --- a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java +++ b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java @@ -1,43 +1,59 @@ package net.corda.node.services.vault; -import com.google.common.collect.*; -import kotlin.*; -import net.corda.contracts.*; -import net.corda.contracts.asset.*; +import com.google.common.collect.ImmutableSet; +import kotlin.Pair; +import net.corda.contracts.DealState; +import net.corda.contracts.asset.Cash; import net.corda.core.contracts.*; -import net.corda.core.crypto.*; -import net.corda.core.identity.*; +import net.corda.core.contracts.testing.DummyLinearContract; +import net.corda.core.crypto.SecureHash; +import net.corda.core.identity.AbstractParty; import net.corda.core.messaging.DataFeed; -import net.corda.core.node.services.*; +import net.corda.core.node.services.Vault; +import net.corda.core.node.services.VaultQueryException; +import net.corda.core.node.services.VaultQueryService; +import net.corda.core.node.services.VaultService; import net.corda.core.node.services.vault.*; -import net.corda.core.node.services.vault.QueryCriteria.*; -import net.corda.core.schemas.*; -import net.corda.core.serialization.*; -import net.corda.core.transactions.*; -import net.corda.node.services.database.*; -import net.corda.node.services.schema.*; -import net.corda.schemas.*; -import net.corda.testing.node.*; -import org.jetbrains.annotations.*; -import org.jetbrains.exposed.sql.*; -import org.junit.*; +import net.corda.core.node.services.vault.QueryCriteria.LinearStateQueryCriteria; +import net.corda.core.node.services.vault.QueryCriteria.VaultCustomQueryCriteria; +import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria; +import net.corda.core.schemas.MappedSchema; +import net.corda.core.schemas.testing.DummyLinearStateSchemaV1; +import net.corda.core.serialization.OpaqueBytes; +import net.corda.core.transactions.SignedTransaction; +import net.corda.core.transactions.WireTransaction; +import net.corda.node.services.database.HibernateConfiguration; +import net.corda.node.services.schema.NodeSchemaService; +import net.corda.schemas.CashSchemaV1; +import net.corda.testing.node.MockServices; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.exposed.sql.Database; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; import rx.Observable; -import java.io.*; -import java.lang.reflect.*; +import java.io.Closeable; +import java.io.IOException; +import java.lang.reflect.Field; import java.util.*; -import java.util.stream.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; -import static net.corda.contracts.asset.CashKt.*; +import static net.corda.contracts.asset.CashKt.getDUMMY_CASH_ISSUER; +import static net.corda.contracts.asset.CashKt.getDUMMY_CASH_ISSUER_KEY; import static net.corda.contracts.testing.VaultFiller.*; -import static net.corda.core.node.services.vault.QueryCriteriaKt.*; -import static net.corda.core.node.services.vault.QueryCriteriaUtilsKt.*; -import static net.corda.core.utilities.TestConstants.*; -import static net.corda.node.utilities.DatabaseSupportKt.*; +import static net.corda.core.node.services.vault.QueryCriteriaKt.and; +import static net.corda.core.node.services.vault.QueryCriteriaKt.or; +import static net.corda.core.node.services.vault.QueryCriteriaUtilsKt.getMAX_PAGE_SIZE; +import static net.corda.core.utilities.TestConstants.getDUMMY_NOTARY; +import static net.corda.node.utilities.DatabaseSupportKt.configureDatabase; import static net.corda.node.utilities.DatabaseSupportKt.transaction; -import static net.corda.testing.CoreTestUtils.*; -import static net.corda.testing.node.MockServicesKt.*; -import static org.assertj.core.api.Assertions.*; +import static net.corda.testing.CoreTestUtils.getMEGA_CORP; +import static net.corda.testing.CoreTestUtils.getMEGA_CORP_KEY; +import static net.corda.testing.node.MockServicesKt.makeTestDataSourceProperties; +import static org.assertj.core.api.Assertions.assertThat; public class VaultQueryJavaTests { diff --git a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt index 206cccccb4..4ff4962f76 100644 --- a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt @@ -1,6 +1,7 @@ package net.corda.node.services import net.corda.core.contracts.* +import net.corda.core.contracts.testing.DummyContract import net.corda.core.crypto.generateKeyPair import net.corda.core.getOrThrow import net.corda.core.identity.Party diff --git a/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt index 4cf8b2746b..ff5bfa324c 100644 --- a/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt @@ -10,9 +10,9 @@ import net.corda.core.contracts.* import net.corda.core.crypto.toBase58String import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultService -import net.corda.core.schemas.DummyLinearStateSchemaV1 -import net.corda.core.schemas.DummyLinearStateSchemaV2 import net.corda.core.schemas.PersistentStateRef +import net.corda.core.schemas.testing.DummyLinearStateSchemaV1 +import net.corda.core.schemas.testing.DummyLinearStateSchemaV2 import net.corda.core.serialization.deserialize import net.corda.core.serialization.storageKryo import net.corda.core.transactions.SignedTransaction diff --git a/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt index b1b482261a..1ad35060e0 100644 --- a/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt @@ -3,12 +3,12 @@ package net.corda.node.services.database import io.requery.Persistable import io.requery.kotlin.eq import io.requery.sql.KotlinEntityDataStore -import net.corda.core.contracts.DummyContract import net.corda.core.contracts.StateRef import net.corda.core.contracts.TransactionType +import net.corda.core.contracts.testing.DummyContract import net.corda.core.crypto.DigitalSignature -import net.corda.core.crypto.NullPublicKey import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.testing.NullPublicKey import net.corda.core.crypto.toBase58String import net.corda.core.identity.AnonymousParty import net.corda.core.node.services.Vault diff --git a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt index f818364974..e271045133 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt @@ -2,6 +2,7 @@ package net.corda.node.services.events import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.* +import net.corda.core.contracts.testing.DummyContract import net.corda.core.crypto.containsAny import net.corda.core.flows.FlowInitiator import net.corda.core.flows.FlowLogic diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt index 24c2b17dba..232db57467 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt @@ -3,8 +3,8 @@ package net.corda.node.services.persistence import net.corda.core.contracts.StateRef import net.corda.core.contracts.TransactionType import net.corda.core.crypto.DigitalSignature -import net.corda.core.crypto.NullPublicKey import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.testing.NullPublicKey import net.corda.core.toFuture import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt index 409ae70201..54f97ba873 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt @@ -7,11 +7,14 @@ import net.corda.contracts.asset.Cash import net.corda.core.* import net.corda.core.contracts.ContractState import net.corda.core.contracts.DOLLARS -import net.corda.core.contracts.DummyState import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.testing.DummyState import net.corda.core.crypto.SecureHash import net.corda.core.crypto.generateKeyPair -import net.corda.core.flows.* +import net.corda.core.flows.FlowException +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSessionException +import net.corda.core.flows.InitiatingFlow import net.corda.core.identity.Party import net.corda.core.messaging.MessageRecipients import net.corda.core.node.services.PartyInfo diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt index 45ed3fc9d5..deaa8bc278 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt @@ -1,10 +1,10 @@ package net.corda.node.services.transactions import com.google.common.util.concurrent.ListenableFuture -import net.corda.core.contracts.DummyContract import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef import net.corda.core.contracts.TransactionType +import net.corda.core.contracts.testing.DummyContract import net.corda.core.crypto.DigitalSignature import net.corda.core.getOrThrow import net.corda.core.node.services.ServiceInfo diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt index 3d6f495650..267632521a 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt @@ -1,7 +1,11 @@ package net.corda.node.services.transactions import com.google.common.util.concurrent.ListenableFuture -import net.corda.core.contracts.* +import net.corda.core.contracts.Command +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TransactionType +import net.corda.core.contracts.testing.DummyContract import net.corda.core.crypto.DigitalSignature import net.corda.core.getOrThrow import net.corda.core.node.services.ServiceInfo diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index 3183a5546a..f36e44708e 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -8,13 +8,14 @@ import net.corda.contracts.asset.Cash import net.corda.contracts.asset.DUMMY_CASH_ISSUER import net.corda.contracts.testing.* import net.corda.core.contracts.* +import net.corda.core.contracts.testing.DummyLinearContract import net.corda.core.crypto.entropyToKeyPair import net.corda.core.days import net.corda.core.identity.Party import net.corda.core.node.services.* import net.corda.core.node.services.vault.* import net.corda.core.node.services.vault.QueryCriteria.* -import net.corda.core.schemas.DummyLinearStateSchemaV1 +import net.corda.core.schemas.testing.DummyLinearStateSchemaV1 import net.corda.core.seconds import net.corda.core.serialization.OpaqueBytes import net.corda.core.transactions.SignedTransaction @@ -706,7 +707,7 @@ class VaultQueryTests { } assertThat(states).hasSize(20) - assertThat(metadata.first().contractStateClassName).isEqualTo("net.corda.core.contracts.DummyLinearContract\$State") + assertThat(metadata.first().contractStateClassName).isEqualTo("net.corda.core.contracts.testing.DummyLinearContract\$State") assertThat(metadata.first().status).isEqualTo(Vault.StateStatus.UNCONSUMED) // 0 = UNCONSUMED assertThat(metadata.last().contractStateClassName).isEqualTo("net.corda.contracts.DummyDealContract\$State") assertThat(metadata.last().status).isEqualTo(Vault.StateStatus.CONSUMED) // 1 = CONSUMED diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt index e857b2f5ca..5e08269b5b 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt @@ -7,6 +7,7 @@ import net.corda.contracts.testing.fillWithSomeTestCash import net.corda.contracts.testing.fillWithSomeTestDeals import net.corda.contracts.testing.fillWithSomeTestLinearStates import net.corda.core.contracts.* +import net.corda.core.contracts.testing.DummyLinearContract import net.corda.core.identity.AnonymousParty import net.corda.core.node.services.VaultService import net.corda.core.node.services.consumedStates @@ -22,7 +23,8 @@ import net.corda.testing.MEGA_CORP import net.corda.testing.MEGA_CORP_KEY import net.corda.testing.node.MockServices import net.corda.testing.node.makeTestDataSourceProperties -import org.assertj.core.api.Assertions.* +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy import org.jetbrains.exposed.sql.Database import org.junit.After import org.junit.Before diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt index cb7bb6196c..bfd75f00d5 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt @@ -1,10 +1,10 @@ package net.corda.notarydemo.flows import co.paralleluniverse.fibers.Suspendable -import net.corda.core.contracts.DummyContract -import net.corda.core.identity.Party +import net.corda.core.contracts.testing.DummyContract import net.corda.core.flows.FlowLogic import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.Party import net.corda.core.transactions.SignedTransaction import java.util.* diff --git a/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt b/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt index b132d6516f..a12f813779 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt @@ -2,13 +2,13 @@ package net.corda.testing import net.corda.core.contracts.* import net.corda.core.crypto.* +import net.corda.core.crypto.testing.NullSignature import net.corda.core.identity.Party import net.corda.core.node.ServiceHub import net.corda.core.serialization.serialize import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.DUMMY_NOTARY_KEY import java.io.InputStream import java.security.KeyPair import java.security.PublicKey diff --git a/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt b/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt index b806e699de..4e8d63212a 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt @@ -1,6 +1,7 @@ package net.corda.testing import net.corda.core.contracts.* +import net.corda.core.contracts.testing.DummyContract import net.corda.core.crypto.SecureHash import net.corda.core.identity.Party import net.corda.core.seconds diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt index 00c1773c6b..5638dc8e1b 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt @@ -6,7 +6,7 @@ import net.corda.client.mock.pickOne import net.corda.client.mock.replicate import net.corda.contracts.asset.DUMMY_CASH_ISSUER import net.corda.contracts.asset.DUMMY_CASH_ISSUER_KEY -import net.corda.core.contracts.DummyContract +import net.corda.core.contracts.testing.DummyContract import net.corda.core.flows.FlowException import net.corda.core.messaging.startFlow import net.corda.core.success diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt index 78a84a497d..8962c5d230 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt @@ -2,7 +2,11 @@ package net.corda.verifier import net.corda.client.mock.* import net.corda.core.contracts.* -import net.corda.core.crypto.* +import net.corda.core.contracts.testing.DummyContract +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.X509Utilities +import net.corda.core.crypto.entropyToKeyPair +import net.corda.core.crypto.sha256 import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party From f732d2cefea68360a60915e4e27e45f551a10352 Mon Sep 17 00:00:00 2001 From: josecoll Date: Wed, 5 Jul 2017 10:01:35 +0100 Subject: [PATCH 46/97] FIX Vault Query defaults to UNCONSUMED in all QueryCriteria types (#958) * Fix https://github.com/corda/corda/issues/949 by providing a default StateStatus argument to all QueryCriteria types. * Abstracted Common Criteria into its own abstract data class + associated visitor. * Incorporating feedback from RP PR review. --- .../core/node/services/vault/QueryCriteria.kt | 60 ++++++++++--------- docs/source/api-vault-query.rst | 2 + .../vault/HibernateQueryCriteriaParser.kt | 22 ++++--- .../services/vault/VaultQueryJavaTests.java | 6 +- .../node/services/vault/VaultQueryTests.kt | 54 ++++++++++++----- 5 files changed, 90 insertions(+), 54 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt index 9dd0e974d1..6677352c6c 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt @@ -26,32 +26,36 @@ sealed class QueryCriteria { @CordaSerializable data class TimeCondition(val type: TimeInstantType, val predicate: ColumnPredicate) - /** - * VaultQueryCriteria: provides query by attributes defined in [VaultSchema.VaultStates] - */ - data class VaultQueryCriteria @JvmOverloads constructor ( - val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, - val contractStateTypes: Set>? = null, - val stateRefs: List? = null, - val notaryName: List? = null, - val includeSoftlockedStates: Boolean = true, - val timeCondition: TimeCondition? = null) : QueryCriteria() { - + abstract class CommonQueryCriteria : QueryCriteria() { + abstract val status: Vault.StateStatus override fun visit(parser: IQueryCriteriaParser): Collection { return parser.parseCriteria(this) } } + /** + * VaultQueryCriteria: provides query by attributes defined in [VaultSchema.VaultStates] + */ + data class VaultQueryCriteria @JvmOverloads constructor (override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, + val contractStateTypes: Set>? = null, + val stateRefs: List? = null, + val notaryName: List? = null, + val includeSoftlockedStates: Boolean = true, + val timeCondition: TimeCondition? = null) : CommonQueryCriteria() { + override fun visit(parser: IQueryCriteriaParser): Collection { + return parser.parseCriteria(this as CommonQueryCriteria).plus(parser.parseCriteria(this)) + } + } + /** * LinearStateQueryCriteria: provides query by attributes defined in [VaultSchema.VaultLinearState] */ - data class LinearStateQueryCriteria @JvmOverloads constructor( - val participants: List? = null, - val linearId: List? = null, - val dealRef: List? = null) : QueryCriteria() { - + data class LinearStateQueryCriteria @JvmOverloads constructor(val participants: List? = null, + val linearId: List? = null, + val dealRef: List? = null, + override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED) : CommonQueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { - return parser.parseCriteria(this) + return parser.parseCriteria(this as CommonQueryCriteria).plus(parser.parseCriteria(this)) } } @@ -62,15 +66,14 @@ sealed class QueryCriteria { * [Currency] as used in [Cash] contract state * [Commodity] as used in [CommodityContract] state */ - data class FungibleAssetQueryCriteria @JvmOverloads constructor( - val participants: List? = null, - val owner: List? = null, - val quantity: ColumnPredicate? = null, - val issuerPartyName: List? = null, - val issuerRef: List? = null) : QueryCriteria() { - + data class FungibleAssetQueryCriteria @JvmOverloads constructor(val participants: List? = null, + val owner: List? = null, + val quantity: ColumnPredicate? = null, + val issuerPartyName: List? = null, + val issuerRef: List? = null, + override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED) : CommonQueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { - return parser.parseCriteria(this) + return parser.parseCriteria(this as CommonQueryCriteria).plus(parser.parseCriteria(this)) } } @@ -84,9 +87,11 @@ sealed class QueryCriteria { * * Refer to [CommercialPaper.State] for a concrete example. */ - data class VaultCustomQueryCriteria(val expression: CriteriaExpression) : QueryCriteria() { + data class VaultCustomQueryCriteria @JvmOverloads constructor + (val expression: CriteriaExpression, + override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED) : CommonQueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { - return parser.parseCriteria(this) + return parser.parseCriteria(this as CommonQueryCriteria).plus(parser.parseCriteria(this)) } } @@ -112,6 +117,7 @@ sealed class QueryCriteria { } interface IQueryCriteriaParser { + fun parseCriteria(criteria: QueryCriteria.CommonQueryCriteria): Collection fun parseCriteria(criteria: QueryCriteria.FungibleAssetQueryCriteria): Collection fun parseCriteria(criteria: QueryCriteria.LinearStateQueryCriteria): Collection fun parseCriteria(criteria: QueryCriteria.VaultCustomQueryCriteria): Collection diff --git a/docs/source/api-vault-query.rst b/docs/source/api-vault-query.rst index 3651f06e44..817318380f 100644 --- a/docs/source/api-vault-query.rst +++ b/docs/source/api-vault-query.rst @@ -78,6 +78,8 @@ There are four implementations of this interface which can be chained together t :end-before: DOCEND VaultQueryExample20 All ``QueryCriteria`` implementations are composable using ``and`` and ``or`` operators, as also illustrated above. + +All ``QueryCriteria`` implementations provide an explicitly specifiable ``StateStatus`` attribute which defaults to filtering on UNCONSUMED states. .. note:: Custom contract states that implement the ``Queryable`` interface may now extend common schemas types ``FungiblePersistentState`` or, ``LinearPersistentState``. Previously, all custom contracts extended the root ``PersistentState`` class and defined repeated mappings of ``FungibleAsset`` and ``LinearState`` attributes. See ``SampleCashSchemaV2`` and ``DummyLinearStateSchemaV2`` as examples. diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt index 9698ed58ad..959eeb73b7 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt @@ -7,6 +7,7 @@ import net.corda.core.identity.AbstractParty import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultQueryException import net.corda.core.node.services.vault.* +import net.corda.core.node.services.vault.QueryCriteria.CommonQueryCriteria import net.corda.core.schemas.PersistentState import net.corda.core.schemas.PersistentStateRef import net.corda.core.serialization.OpaqueBytes @@ -41,13 +42,6 @@ class HibernateQueryCriteriaParser(val contractType: Class, log.trace { "Parsing VaultQueryCriteria: $criteria" } val predicateSet = mutableSetOf() - // state status - stateTypes = criteria.status - if (criteria.status == Vault.StateStatus.ALL) - predicateSet.add(vaultStates.get("stateStatus").`in`(setOf(Vault.StateStatus.UNCONSUMED, Vault.StateStatus.CONSUMED))) - else - predicateSet.add(criteriaBuilder.equal(vaultStates.get("stateStatus"), criteria.status)) - // contract State Types val combinedContractTypeTypes = criteria.contractStateTypes?.plus(contractType) ?: setOf(contractType) combinedContractTypeTypes.filter { it.name != ContractState::class.java.name }.let { @@ -217,6 +211,7 @@ class HibernateQueryCriteriaParser(val contractType: Class, val vaultLinearStates = criteriaQuery.from(VaultSchemaV1.VaultLinearStates::class.java) rootEntities.putIfAbsent(VaultSchemaV1.VaultLinearStates::class.java, vaultLinearStates) + val joinPredicate = criteriaBuilder.equal(vaultStates.get("stateRef"), vaultLinearStates.get("stateRef")) joinPredicates.add(joinPredicate) @@ -255,6 +250,7 @@ class HibernateQueryCriteriaParser(val contractType: Class, try { val entityRoot = criteriaQuery.from(entityClass) rootEntities.putIfAbsent(entityClass, entityRoot) + val joinPredicate = criteriaBuilder.equal(vaultStates.get("stateRef"), entityRoot.get("stateRef")) joinPredicates.add(joinPredicate) @@ -315,6 +311,18 @@ class HibernateQueryCriteriaParser(val contractType: Class, return predicateSet } + override fun parseCriteria(criteria: CommonQueryCriteria): Collection { + log.trace { "Parsing CommonQueryCriteria: $criteria" } + val predicateSet = mutableSetOf() + + // state status + stateTypes = criteria.status + if (criteria.status != Vault.StateStatus.ALL) + predicateSet.add(criteriaBuilder.equal(vaultStates.get("stateStatus"), criteria.status)) + + return predicateSet + } + private fun parse(sorting: Sort) { log.trace { "Parsing sorting specification: $sorting" } diff --git a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java index 305a14407a..ec918900cd 100644 --- a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java +++ b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java @@ -279,12 +279,8 @@ public class VaultQueryJavaTests { List linearIds = Arrays.asList(uid); List dealParty = Arrays.asList(getMEGA_CORP()); QueryCriteria dealCriteria = new LinearStateQueryCriteria(dealParty, null, dealIds); - QueryCriteria linearCriteria = new LinearStateQueryCriteria(dealParty, linearIds, null); - - QueryCriteria dealOrLinearIdCriteria = or(dealCriteria, linearCriteria); - QueryCriteria compositeCriteria = and(dealOrLinearIdCriteria, vaultCriteria); PageSpecification pageSpec = new PageSpecification(0, getMAX_PAGE_SIZE()); @@ -296,7 +292,7 @@ public class VaultQueryJavaTests { Observable updates = results.getFuture(); // DOCEND VaultJavaQueryExample5 - assertThat(snapshot.getStates()).hasSize(4); + assertThat(snapshot.getStates()).hasSize(13); return tx; }); diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index 3183a5546a..68589a78da 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -753,6 +753,20 @@ class VaultQueryTests { } } + @Test + fun `unconsumed cash fungible assets after spending`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) + services.consumeCash(50.DOLLARS) + // should now have x2 CONSUMED + x2 UNCONSUMED (one spent + one change) + + val results = vaultQuerySvc.queryBy(FungibleAssetQueryCriteria()) + assertThat(results.statesMetadata).hasSize(2) + assertThat(results.states).hasSize(2) + } + } + @Test fun `consumed cash fungible assets`() { database.transaction { @@ -845,7 +859,7 @@ class VaultQueryTests { // should now have 1 UNCONSUMED & 3 CONSUMED state refs for Linear State with "TEST" // DOCSTART VaultQueryExample9 - val linearStateCriteria = LinearStateQueryCriteria(linearId = listOf(linearId)) + val linearStateCriteria = LinearStateQueryCriteria(linearId = listOf(linearId), status = Vault.StateStatus.ALL) val vaultCriteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) val results = vaultQuerySvc.queryBy(linearStateCriteria.and(vaultCriteria)) // DOCEND VaultQueryExample9 @@ -864,7 +878,7 @@ class VaultQueryTests { services.evolveLinearStates(linearStates) // consume current and produce new state reference // should now have 1 UNCONSUMED & 3 CONSUMED state refs for Linear State with "TEST" - val linearStateCriteria = LinearStateQueryCriteria(linearId = linearStates.map { it.state.data.linearId }) + val linearStateCriteria = LinearStateQueryCriteria(linearId = linearStates.map { it.state.data.linearId }, status = Vault.StateStatus.ALL) val vaultCriteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Standard(Sort.LinearStateAttribute.UUID), Sort.Direction.DESC))) @@ -899,18 +913,15 @@ class VaultQueryTests { val uid = linearStates.states.first().state.data.linearId services.fillWithSomeTestDeals(listOf("123", "456", "789")) - val vaultCriteria = VaultQueryCriteria(status = Vault.StateStatus.UNCONSUMED) val linearStateCriteria = LinearStateQueryCriteria(linearId = listOf(uid)) val dealStateCriteria = LinearStateQueryCriteria(dealRef = listOf("123", "456", "789")) - val compositeCriteria = vaultCriteria.and(linearStateCriteria).or(dealStateCriteria) + val compositeCriteria = linearStateCriteria or dealStateCriteria val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Standard(Sort.LinearStateAttribute.DEAL_REFERENCE), Sort.Direction.DESC))) val results = vaultQuerySvc.queryBy(compositeCriteria, sorting = sorting) - results.states.forEach { - if (it.state.data is DummyDealContract.State) - println("${(it.state.data as DealState).ref}, ${it.state.data.linearId}") } - assertThat(results.states).hasSize(4) + assertThat(results.statesMetadata).hasSize(13) + assertThat(results.states).hasSize(13) } } @@ -942,7 +953,7 @@ class VaultQueryTests { services.evolveLinearState(linearState3) // consume current and produce new state reference // should now have 1 UNCONSUMED & 3 CONSUMED state refs for Linear State with "TEST" - val linearStateCriteria = LinearStateQueryCriteria(linearId = txns.states.map { it.state.data.linearId }) + val linearStateCriteria = LinearStateQueryCriteria(linearId = txns.states.map { it.state.data.linearId }, status = Vault.StateStatus.CONSUMED) val vaultCriteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Standard(Sort.LinearStateAttribute.UUID), Sort.Direction.DESC))) val results = vaultQuerySvc.queryBy(linearStateCriteria.and(vaultCriteria), sorting = sorting) @@ -966,8 +977,12 @@ class VaultQueryTests { // DOCSTART VaultDeprecatedQueryExample1 val states = vaultSvc.linearHeadsOfType().filter { it.key == linearId } // DOCEND VaultDeprecatedQueryExample1 - assertThat(states).hasSize(1) + + // validate against new query api + val results = vaultQuerySvc.queryBy(LinearStateQueryCriteria(linearId = listOf(linearId))) + assertThat(results.statesMetadata).hasSize(1) + assertThat(results.states).hasSize(1) } } @@ -987,8 +1002,12 @@ class VaultQueryTests { // DOCSTART VaultDeprecatedQueryExample2 val states = vaultSvc.consumedStates().filter { it.state.data.linearId == linearId } // DOCEND VaultDeprecatedQueryExample2 - assertThat(states).hasSize(3) + + // validate against new query api + val results = vaultQuerySvc.queryBy(LinearStateQueryCriteria(linearId = listOf(linearId), status = Vault.StateStatus.CONSUMED)) + assertThat(results.statesMetadata).hasSize(3) + assertThat(results.states).hasSize(3) } } @@ -1009,8 +1028,12 @@ class VaultQueryTests { val states = vaultSvc.states(setOf(DummyLinearContract.State::class.java), EnumSet.of(Vault.StateStatus.CONSUMED, Vault.StateStatus.UNCONSUMED)).filter { it.state.data.linearId == linearId } // DOCEND VaultDeprecatedQueryExample3 - assertThat(states).hasSize(4) + + // validate against new query api + val results = vaultQuerySvc.queryBy(LinearStateQueryCriteria(linearId = listOf(linearId), status = Vault.StateStatus.ALL)) + assertThat(results.statesMetadata).hasSize(4) + assertThat(results.states).hasSize(4) } } @@ -1420,7 +1443,7 @@ class VaultQueryTests { services.fillWithSomeTestLinearStates(1, "TEST2") val uuid = services.fillWithSomeTestLinearStates(1, "TEST3").states.first().state.data.linearId.id - // 2 unconsumed states with same external ID + // 2 unconsumed states with same external ID, 1 with different external ID val results = builder { val externalIdCondition = VaultSchemaV1.VaultLinearStates::externalId.equal("TEST2") @@ -1429,10 +1452,11 @@ class VaultQueryTests { val uuidCondition = VaultSchemaV1.VaultLinearStates::uuid.equal(uuid) val uuidCustomCriteria = VaultCustomQueryCriteria(uuidCondition) - val criteria = externalIdCustomCriteria.or(uuidCustomCriteria) + val criteria = externalIdCustomCriteria or uuidCustomCriteria vaultQuerySvc.queryBy(criteria) } - assertThat(results.states).hasSize(2) + assertThat(results.statesMetadata).hasSize(3) + assertThat(results.states).hasSize(3) } } From 6dc7f694e4dce6dae4cbf2bfee7f8c8c9a5b43fb Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Thu, 29 Jun 2017 17:53:07 +0100 Subject: [PATCH 47/97] Add explicit support for nullable types Remove prohibition against non string object classes such as arrays Squashed Commmits: * Tidyup whitespace * WIP * Review Comments * WIP - adding concept of nullabltily into the carpenter * Add explicit nullable and non nullable fields * Rebase onto master, fix package names in carpenter --- .../serialization/carpenter/ClassCarpenter.kt | 168 +++++++-- .../carpenter/ClassCarpenterTest.kt | 325 ++++++++++++++++-- 2 files changed, 422 insertions(+), 71 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/serialization/carpenter/ClassCarpenter.kt b/core/src/main/kotlin/net/corda/core/serialization/carpenter/ClassCarpenter.kt index 534083325f..46bbafff3b 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/carpenter/ClassCarpenter.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/carpenter/ClassCarpenter.kt @@ -1,4 +1,4 @@ -package net.corda.carpenter +package net.corda.core.serialization.carpenter import org.objectweb.asm.ClassWriter import org.objectweb.asm.MethodVisitor @@ -60,23 +60,118 @@ interface SimpleFieldAccess { * * Equals/hashCode methods are not yet supported. */ + +fun Map.descriptors() = LinkedHashMap(this.mapValues { it.value.descriptor }) + class ClassCarpenter { - // TODO: Array types. // TODO: Generics. // TODO: Sandbox the generated code when a security manager is in use. // TODO: Generate equals/hashCode. // TODO: Support annotations. // TODO: isFoo getter patterns for booleans (this is what Kotlin generates) + + class DuplicateName : RuntimeException("An attempt was made to register two classes with the same name within the same ClassCarpenter namespace.") + class InterfaceMismatch(msg: String) : RuntimeException(msg) + class NullablePrimitive(msg: String) : RuntimeException(msg) + + abstract class Field(val field: Class) { + val unsetName = "Unset" + var name: String = unsetName + abstract val nullabilityAnnotation: String + + val descriptor: String + get() = Type.getDescriptor(this.field) + + val type: String + get() = if (this.field.isPrimitive) this.descriptor else "Ljava/lang/Object;" + + fun generateField(cw: ClassWriter) { + val fieldVisitor = cw.visitField(ACC_PROTECTED + ACC_FINAL, name, descriptor, null, null) + cw.visitAnnotation(nullabilityAnnotation, false).visitEnd() + fieldVisitor.visitEnd() + } + + fun addNullabilityAnnotation(mv: MethodVisitor) { + mv.visitAnnotation(nullabilityAnnotation, false) + } + + abstract fun copy(name: String, field: Class): Field + abstract fun nullTest(mv: MethodVisitor, slot: Int) + fun visitParameter(mv: MethodVisitor, idx: Int) { + with(mv) { + visitParameter(name, 0) + if (!field.isPrimitive) { + visitParameterAnnotation(idx, nullabilityAnnotation, false).visitEnd() + } + } + } + } + + class NonNullableField(field: Class) : Field(field) { + override val nullabilityAnnotation = "Lorg/jetbrains/annotations/Nullable;" + + constructor(name: String, field: Class) : this(field) { + this.name = name + } + + override fun copy(name: String, field: Class) = NonNullableField(name, field) + + override fun nullTest(mv: MethodVisitor, slot: Int) { + assert(name != unsetName) + + if (!field.isPrimitive) { + with(mv) { + visitVarInsn(ALOAD, 0) // load this + visitVarInsn(ALOAD, slot) // load parameter + visitLdcInsn("param \"$name\" cannot be null") + visitMethodInsn(INVOKESTATIC, + "java/util/Objects", + "requireNonNull", + "(Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Object;", false) + visitInsn(POP) + } + } + } + } + + + class NullableField(field: Class) : Field(field) { + override val nullabilityAnnotation = "Lorg/jetbrains/annotations/NotNull;" + + constructor(name: String, field: Class) : this(field) { + if (field.isPrimitive) { + throw NullablePrimitive ( + "Field $name is primitive type ${Type.getDescriptor(field)} and thus cannot be nullable") + } + + this.name = name + } + + override fun copy(name: String, field: Class) = NullableField(name, field) + + override fun nullTest(mv: MethodVisitor, slot: Int) { + assert(name != unsetName) + } + } + /** * A Schema represents a desired class. */ - open class Schema(val name: String, fields: Map>, val superclass: Schema? = null, val interfaces: List> = emptyList()) { - val fields = LinkedHashMap(fields) // Fix the order up front if the user didn't. - val descriptors = fields.map { it.key to Type.getDescriptor(it.value) }.toMap() + abstract class Schema( + val name: String, + fields: Map, + val superclass: Schema? = null, + val interfaces: List> = emptyList()) { + /* Fix the order up front if the user didn't, inject the name into the field as it's + neater when iterating */ + val fields = LinkedHashMap(fields.mapValues { it.value.copy(it.key, it.value.field) }) - fun fieldsIncludingSuperclasses(): Map> = (superclass?.fieldsIncludingSuperclasses() ?: emptyMap()) + LinkedHashMap(fields) - fun descriptorsIncludingSuperclasses(): Map = (superclass?.descriptorsIncludingSuperclasses() ?: emptyMap()) + LinkedHashMap(descriptors) + fun fieldsIncludingSuperclasses(): Map = + (superclass?.fieldsIncludingSuperclasses() ?: emptyMap()) + LinkedHashMap(fields) + + fun descriptorsIncludingSuperclasses(): Map = + (superclass?.descriptorsIncludingSuperclasses() ?: emptyMap()) + fields.descriptors() val jvmName: String get() = name.replace(".", "/") @@ -86,21 +181,18 @@ class ClassCarpenter { class ClassSchema( name: String, - fields: Map>, + fields: Map, superclass: Schema? = null, interfaces: List> = emptyList() ) : Schema(name, fields, superclass, interfaces) class InterfaceSchema( name: String, - fields: Map>, + fields: Map, superclass: Schema? = null, interfaces: List> = emptyList() ) : Schema(name, fields, superclass, interfaces) - class DuplicateName : RuntimeException("An attempt was made to register two classes with the same name within the same ClassCarpenter namespace.") - class InterfaceMismatch(msg: String) : RuntimeException(msg) - private class CarpenterClassLoader : ClassLoader(Thread.currentThread().contextClassLoader) { fun load(name: String, bytes: ByteArray) = defineClass(name, bytes, 0, bytes.size) } @@ -186,9 +278,7 @@ class ClassCarpenter { } private fun ClassWriter.generateFields(schema: Schema) { - for ((name, desc) in schema.descriptors) { - visitField(ACC_PROTECTED + ACC_FINAL, name, desc, null, null).visitEnd() - } + schema.fields.forEach { it.value.generateField(this) } } private fun ClassWriter.generateToString(schema: Schema) { @@ -199,12 +289,11 @@ class ClassCarpenter { visitLdcInsn(schema.name.split('.').last()) visitMethodInsn(INVOKESTATIC, "com/google/common/base/MoreObjects", "toStringHelper", "(Ljava/lang/String;)L$toStringHelper;", false) // Call the add() methods. - for ((name, type) in schema.fieldsIncludingSuperclasses().entries) { + for ((name, field) in schema.fieldsIncludingSuperclasses().entries) { visitLdcInsn(name) visitVarInsn(ALOAD, 0) // this visitFieldInsn(GETFIELD, schema.jvmName, name, schema.descriptorsIncludingSuperclasses()[name]) - val desc = if (type.isPrimitive) schema.descriptors[name] else "Ljava/lang/Object;" - visitMethodInsn(INVOKEVIRTUAL, toStringHelper, "add", "(Ljava/lang/String;$desc)L$toStringHelper;", false) + visitMethodInsn(INVOKEVIRTUAL, toStringHelper, "add", "(Ljava/lang/String;${field.type})L$toStringHelper;", false) } // call toString() on the builder and return. visitMethodInsn(INVOKEVIRTUAL, toStringHelper, "toString", "()Ljava/lang/String;", false) @@ -232,12 +321,12 @@ class ClassCarpenter { private fun ClassWriter.generateGetters(schema: Schema) { for ((name, type) in schema.fields) { - val descriptor = schema.descriptors[name] - with(visitMethod(ACC_PUBLIC, "get" + name.capitalize(), "()" + descriptor, null, null)) { + with(visitMethod(ACC_PUBLIC, "get" + name.capitalize(), "()" + type.descriptor, null, null)) { + type.addNullabilityAnnotation(this) visitCode() visitVarInsn(ALOAD, 0) // Load 'this' - visitFieldInsn(GETFIELD, schema.jvmName, name, descriptor) - when (type) { + visitFieldInsn(GETFIELD, schema.jvmName, name, type.descriptor) + when (type.field) { java.lang.Boolean.TYPE, Integer.TYPE, java.lang.Short.TYPE, java.lang.Byte.TYPE, TYPE -> visitInsn(IRETURN) java.lang.Long.TYPE -> visitInsn(LRETURN) java.lang.Double.TYPE -> visitInsn(DRETURN) @@ -251,8 +340,8 @@ class ClassCarpenter { } private fun ClassWriter.generateAbstractGetters(schema: Schema) { - for ((name, _) in schema.fields) { - val descriptor = schema.descriptors[name] + for ((name, field) in schema.fields) { + val descriptor = field.descriptor val opcodes = ACC_ABSTRACT + ACC_PUBLIC with(visitMethod(opcodes, "get" + name.capitalize(), "()" + descriptor, null, null)) { // abstract method doesn't have any implementation so just end @@ -262,8 +351,18 @@ class ClassCarpenter { } private fun ClassWriter.generateConstructor(schema: Schema) { - with(visitMethod(ACC_PUBLIC, "", "(" + schema.descriptorsIncludingSuperclasses().values.joinToString("") + ")V", null, null)) { + with(visitMethod( + ACC_PUBLIC, + "", + "(" + schema.descriptorsIncludingSuperclasses().values.joinToString("") + ")V", + null, + null)) + { + var idx = 0 + schema.fields.values.forEach { it.visitParameter(this, idx++) } + visitCode() + // Calculate the super call. val superclassFields = schema.superclass?.fieldsIncludingSuperclasses() ?: emptyMap() visitVarInsn(ALOAD, 0) @@ -276,14 +375,16 @@ class ClassCarpenter { val superDesc = schema.superclass.descriptorsIncludingSuperclasses().values.joinToString("") visitMethodInsn(INVOKESPECIAL, schema.superclass.name.jvm, "", "($superDesc)V", false) } + // Assign the fields from parameters. var slot = 1 + superclassFields.size - for ((name, type) in schema.fields.entries) { - if (type.isArray) - throw UnsupportedOperationException("Array types are not implemented yet") + + for ((name, field) in schema.fields.entries) { + field.nullTest(this, slot) + visitVarInsn(ALOAD, 0) // Load 'this' onto the stack - slot += load(slot, type) // Load the contents of the parameter onto the stack. - visitFieldInsn(PUTFIELD, schema.jvmName, name, schema.descriptors[name]) + slot += load(slot, field) // Load the contents of the parameter onto the stack. + visitFieldInsn(PUTFIELD, schema.jvmName, name, field.descriptor) } visitInsn(RETURN) visitMaxs(0, 0) @@ -291,16 +392,15 @@ class ClassCarpenter { } } - // Returns how many slots the given type takes up. - private fun MethodVisitor.load(slot: Int, type: Class): Int { - when (type) { + private fun MethodVisitor.load(slot: Int, type: Field): Int { + when (type.field) { java.lang.Boolean.TYPE, Integer.TYPE, java.lang.Short.TYPE, java.lang.Byte.TYPE, TYPE -> visitVarInsn(ILOAD, slot) java.lang.Long.TYPE -> visitVarInsn(LLOAD, slot) java.lang.Double.TYPE -> visitVarInsn(DLOAD, slot) java.lang.Float.TYPE -> visitVarInsn(FLOAD, slot) else -> visitVarInsn(ALOAD, slot) } - return when (type) { + return when (type.field) { java.lang.Long.TYPE, java.lang.Double.TYPE -> 2 else -> 1 } diff --git a/core/src/test/kotlin/net/corda/core/serialization/carpenter/ClassCarpenterTest.kt b/core/src/test/kotlin/net/corda/core/serialization/carpenter/ClassCarpenterTest.kt index 1827c83d79..8baa7fd4ae 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/carpenter/ClassCarpenterTest.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/carpenter/ClassCarpenterTest.kt @@ -1,4 +1,5 @@ -package net.corda.carpenter +package net.corda.core.serialization.carpenter + import org.junit.Test import java.lang.reflect.Field @@ -30,16 +31,19 @@ class ClassCarpenterTest { @Test fun prims() { - val clazz = cc.build(ClassCarpenter.ClassSchema("gen.Prims", mapOf( - "anIntField" to Int::class.javaPrimitiveType!!, - "aLongField" to Long::class.javaPrimitiveType!!, - "someCharField" to Char::class.javaPrimitiveType!!, - "aShortField" to Short::class.javaPrimitiveType!!, - "doubleTrouble" to Double::class.javaPrimitiveType!!, - "floatMyBoat" to Float::class.javaPrimitiveType!!, - "byteMe" to Byte::class.javaPrimitiveType!!, - "booleanField" to Boolean::class.javaPrimitiveType!! - ))) + val clazz = cc.build(ClassCarpenter.ClassSchema( + "gen.Prims", + mapOf( + "anIntField" to Int::class.javaPrimitiveType!!, + "aLongField" to Long::class.javaPrimitiveType!!, + "someCharField" to Char::class.javaPrimitiveType!!, + "aShortField" to Short::class.javaPrimitiveType!!, + "doubleTrouble" to Double::class.javaPrimitiveType!!, + "floatMyBoat" to Float::class.javaPrimitiveType!!, + "byteMe" to Byte::class.javaPrimitiveType!!, + "booleanField" to Boolean::class.javaPrimitiveType!!).mapValues { + ClassCarpenter.NonNullableField (it.value) + })) assertEquals(8, clazz.nonSyntheticFields.size) assertEquals(10, clazz.nonSyntheticMethods.size) assertEquals(8, clazz.declaredConstructors[0].parameterCount) @@ -68,7 +72,7 @@ class ClassCarpenterTest { val clazz = cc.build(ClassCarpenter.ClassSchema("gen.Person", mapOf( "age" to Int::class.javaPrimitiveType!!, "name" to String::class.java - ))) + ).mapValues { ClassCarpenter.NonNullableField (it.value) } )) val i = clazz.constructors[0].newInstance(32, "Mike") return Pair(clazz, i) } @@ -82,7 +86,7 @@ class ClassCarpenterTest { @Test fun `generated toString`() { - val (clazz, i) = genPerson() + val (_, i) = genPerson() assertEquals("Person{age=32, name=Mike}", i.toString()) } @@ -96,7 +100,7 @@ class ClassCarpenterTest { fun `can refer to each other`() { val (clazz1, i) = genPerson() val clazz2 = cc.build(ClassCarpenter.ClassSchema("gen.Referee", mapOf( - "ref" to clazz1 + "ref" to ClassCarpenter.NonNullableField (clazz1) ))) val i2 = clazz2.constructors[0].newInstance(i) assertEquals(i, (i2 as SimpleFieldAccess)["ref"]) @@ -104,8 +108,15 @@ class ClassCarpenterTest { @Test fun superclasses() { - val schema1 = ClassCarpenter.ClassSchema("gen.A", mapOf("a" to String::class.java)) - val schema2 = ClassCarpenter.ClassSchema("gen.B", mapOf("b" to String::class.java), schema1) + val schema1 = ClassCarpenter.ClassSchema( + "gen.A", + mapOf("a" to ClassCarpenter.NonNullableField (String::class.java))) + + val schema2 = ClassCarpenter.ClassSchema( + "gen.B", + mapOf("b" to ClassCarpenter.NonNullableField (String::class.java)), + schema1) + val clazz = cc.build(schema2) val i = clazz.constructors[0].newInstance("xa", "xb") as SimpleFieldAccess assertEquals("xa", i["a"]) @@ -115,8 +126,14 @@ class ClassCarpenterTest { @Test fun interfaces() { - val schema1 = ClassCarpenter.ClassSchema("gen.A", mapOf("a" to String::class.java)) - val schema2 = ClassCarpenter.ClassSchema("gen.B", mapOf("b" to Int::class.java), schema1, interfaces = listOf(DummyInterface::class.java)) + val schema1 = ClassCarpenter.ClassSchema( + "gen.A", + mapOf("a" to ClassCarpenter.NonNullableField(String::class.java))) + + val schema2 = ClassCarpenter.ClassSchema("gen.B", + mapOf("b" to ClassCarpenter.NonNullableField(Int::class.java)), + schema1, + interfaces = listOf(DummyInterface::class.java)) val clazz = cc.build(schema2) val i = clazz.constructors[0].newInstance("xa", 1) as DummyInterface assertEquals("xa", i.a) @@ -125,8 +142,16 @@ class ClassCarpenterTest { @Test(expected = ClassCarpenter.InterfaceMismatch::class) fun `mismatched interface`() { - val schema1 = ClassCarpenter.ClassSchema("gen.A", mapOf("a" to String::class.java)) - val schema2 = ClassCarpenter.ClassSchema("gen.B", mapOf("c" to Int::class.java), schema1, interfaces = listOf(DummyInterface::class.java)) + val schema1 = ClassCarpenter.ClassSchema( + "gen.A", + mapOf("a" to ClassCarpenter.NonNullableField(String::class.java))) + + val schema2 = ClassCarpenter.ClassSchema( + "gen.B", + mapOf("c" to ClassCarpenter.NonNullableField(Int::class.java)), + schema1, + interfaces = listOf(DummyInterface::class.java)) + val clazz = cc.build(schema2) val i = clazz.constructors[0].newInstance("xa", 1) as DummyInterface assertEquals(1, i.b) @@ -134,15 +159,22 @@ class ClassCarpenterTest { @Test fun `generate interface`() { - val schema1 = ClassCarpenter.InterfaceSchema("gen.Interface", mapOf("a" to Int::class.java)) + val schema1 = ClassCarpenter.InterfaceSchema( + "gen.Interface", + mapOf("a" to ClassCarpenter.NonNullableField (Int::class.java))) + val iface = cc.build(schema1) - assert(iface.isInterface()) + assert(iface.isInterface) assert(iface.constructors.isEmpty()) assertEquals(iface.declaredMethods.size, 1) assertEquals(iface.declaredMethods[0].name, "getA") - val schema2 = ClassCarpenter.ClassSchema("gen.Derived", mapOf("a" to Int::class.java), interfaces = listOf(iface)) + val schema2 = ClassCarpenter.ClassSchema( + "gen.Derived", + mapOf("a" to ClassCarpenter.NonNullableField (Int::class.java)), + interfaces = listOf(iface)) + val clazz = cc.build(schema2) val testA = 42 val i = clazz.constructors[0].newInstance(testA) as SimpleFieldAccess @@ -152,16 +184,25 @@ class ClassCarpenterTest { @Test fun `generate multiple interfaces`() { - val iFace1 = ClassCarpenter.InterfaceSchema("gen.Interface1", mapOf("a" to Int::class.java, "b" to String::class.java)) - val iFace2 = ClassCarpenter.InterfaceSchema("gen.Interface2", mapOf("c" to Int::class.java, "d" to String::class.java)) + val iFace1 = ClassCarpenter.InterfaceSchema( + "gen.Interface1", + mapOf( + "a" to ClassCarpenter.NonNullableField(Int::class.java), + "b" to ClassCarpenter.NonNullableField(String::class.java))) + + val iFace2 = ClassCarpenter.InterfaceSchema( + "gen.Interface2", + mapOf( + "c" to ClassCarpenter.NonNullableField(Int::class.java), + "d" to ClassCarpenter.NonNullableField(String::class.java))) val class1 = ClassCarpenter.ClassSchema( "gen.Derived", mapOf( - "a" to Int::class.java, - "b" to String::class.java, - "c" to Int::class.java, - "d" to String::class.java), + "a" to ClassCarpenter.NonNullableField(Int::class.java), + "b" to ClassCarpenter.NonNullableField(String::class.java), + "c" to ClassCarpenter.NonNullableField(Int::class.java), + "d" to ClassCarpenter.NonNullableField(String::class.java)), interfaces = listOf(cc.build(iFace1), cc.build(iFace2))) val clazz = cc.build(class1) @@ -182,23 +223,23 @@ class ClassCarpenterTest { val iFace1 = ClassCarpenter.InterfaceSchema( "gen.Interface1", mapOf( - "a" to Int::class.java, - "b" to String::class.java)) + "a" to ClassCarpenter.NonNullableField (Int::class.java), + "b" to ClassCarpenter.NonNullableField(String::class.java))) val iFace2 = ClassCarpenter.InterfaceSchema( "gen.Interface2", mapOf( - "c" to Int::class.java, - "d" to String::class.java), + "c" to ClassCarpenter.NonNullableField(Int::class.java), + "d" to ClassCarpenter.NonNullableField(String::class.java)), interfaces = listOf(cc.build(iFace1))) val class1 = ClassCarpenter.ClassSchema( "gen.Derived", mapOf( - "a" to Int::class.java, - "b" to String::class.java, - "c" to Int::class.java, - "d" to String::class.java), + "a" to ClassCarpenter.NonNullableField(Int::class.java), + "b" to ClassCarpenter.NonNullableField(String::class.java), + "c" to ClassCarpenter.NonNullableField(Int::class.java), + "d" to ClassCarpenter.NonNullableField(String::class.java)), interfaces = listOf(cc.build(iFace2))) val clazz = cc.build(class1) @@ -213,4 +254,214 @@ class ClassCarpenterTest { assertEquals(testC, i["c"]) assertEquals(testD, i["d"]) } + + @Test(expected = java.lang.IllegalArgumentException::class) + fun `null parameter small int`() { + val className = "iEnjoySwede" + val schema = ClassCarpenter.ClassSchema( + "gen.$className", + mapOf("a" to ClassCarpenter.NonNullableField (Int::class.java))) + + val clazz = cc.build(schema) + + val a : Int? = null + clazz.constructors[0].newInstance(a) + } + + @Test(expected = ClassCarpenter.NullablePrimitive::class) + fun `nullable parameter small int`() { + val className = "iEnjoySwede" + val schema = ClassCarpenter.ClassSchema( + "gen.$className", + mapOf("a" to ClassCarpenter.NullableField (Int::class.java))) + + cc.build(schema) + } + + @Test + fun `nullable parameter integer`() { + val className = "iEnjoyWibble" + val schema = ClassCarpenter.ClassSchema( + "gen.$className", + mapOf("a" to ClassCarpenter.NullableField (Integer::class.java))) + + val clazz = cc.build(schema) + val a1 : Int? = null + clazz.constructors[0].newInstance(a1) + + val a2 : Int? = 10 + clazz.constructors[0].newInstance(a2) + } + + @Test + fun `non nullable parameter integer with non null`() { + val className = "iEnjoyWibble" + val schema = ClassCarpenter.ClassSchema( + "gen.$className", + mapOf("a" to ClassCarpenter.NonNullableField (Integer::class.java))) + + val clazz = cc.build(schema) + + val a : Int? = 10 + clazz.constructors[0].newInstance(a) + } + + @Test(expected = java.lang.reflect.InvocationTargetException::class) + fun `non nullable parameter integer with null`() { + val className = "iEnjoyWibble" + val schema = ClassCarpenter.ClassSchema( + "gen.$className", + mapOf("a" to ClassCarpenter.NonNullableField (Integer::class.java))) + + val clazz = cc.build(schema) + + val a : Int? = null + clazz.constructors[0].newInstance(a) + } + + @Test + @Suppress("UNCHECKED_CAST") + fun `int array`() { + val className = "iEnjoyPotato" + val schema = ClassCarpenter.ClassSchema( + "gen.$className", + mapOf("a" to ClassCarpenter.NonNullableField(IntArray::class.java))) + + val clazz = cc.build(schema) + + val i = clazz.constructors[0].newInstance(intArrayOf(1, 2, 3)) as SimpleFieldAccess + + val arr = clazz.getMethod("getA").invoke(i) + + assertEquals(1, (arr as IntArray)[0]) + assertEquals(2, arr[1]) + assertEquals(3, arr[2]) + assertEquals("$className{a=[1, 2, 3]}", i.toString()) + } + + @Test(expected = java.lang.reflect.InvocationTargetException::class) + fun `nullable int array throws`() { + val className = "iEnjoySwede" + val schema = ClassCarpenter.ClassSchema( + "gen.$className", + mapOf("a" to ClassCarpenter.NonNullableField(IntArray::class.java))) + + val clazz = cc.build(schema) + + val a : IntArray? = null + clazz.constructors[0].newInstance(a) + } + + @Test + @Suppress("UNCHECKED_CAST") + fun `integer array`() { + val className = "iEnjoyFlan" + val schema = ClassCarpenter.ClassSchema( + "gen.$className", + mapOf("a" to ClassCarpenter.NonNullableField(Array::class.java))) + + val clazz = cc.build(schema) + + val i = clazz.constructors[0].newInstance(arrayOf(1, 2, 3)) as SimpleFieldAccess + + val arr = clazz.getMethod("getA").invoke(i) + + assertEquals(1, (arr as Array)[0]) + assertEquals(2, arr[1]) + assertEquals(3, arr[2]) + assertEquals("$className{a=[1, 2, 3]}", i.toString()) + } + + @Test + @Suppress("UNCHECKED_CAST") + fun `int array with ints`() { + val className = "iEnjoyCrumble" + val schema = ClassCarpenter.ClassSchema( + "gen.$className", mapOf( + "a" to Int::class.java, + "b" to IntArray::class.java, + "c" to Int::class.java).mapValues { ClassCarpenter.NonNullableField(it.value) }) + + val clazz = cc.build(schema) + + val i = clazz.constructors[0].newInstance(2, intArrayOf(4, 8), 16) as SimpleFieldAccess + + assertEquals(2, clazz.getMethod("getA").invoke(i)) + assertEquals(4, (clazz.getMethod("getB").invoke(i) as IntArray)[0]) + assertEquals(8, (clazz.getMethod("getB").invoke(i) as IntArray)[1]) + assertEquals(16, clazz.getMethod("getC").invoke(i)) + + assertEquals("$className{a=2, b=[4, 8], c=16}", i.toString()) + } + + @Test + @Suppress("UNCHECKED_CAST") + fun `multiple int arrays`() { + val className = "iEnjoyJam" + val schema = ClassCarpenter.ClassSchema( + "gen.$className", mapOf( + "a" to IntArray::class.java, + "b" to Int::class.java, + "c" to IntArray::class.java).mapValues { ClassCarpenter.NonNullableField(it.value) }) + + val clazz = cc.build(schema) + val i = clazz.constructors[0].newInstance(intArrayOf(1, 2), 3, intArrayOf(4, 5, 6)) + + assertEquals(1, (clazz.getMethod("getA").invoke(i) as IntArray)[0]) + assertEquals(2, (clazz.getMethod("getA").invoke(i) as IntArray)[1]) + assertEquals(3, clazz.getMethod("getB").invoke(i)) + assertEquals(4, (clazz.getMethod("getC").invoke(i) as IntArray)[0]) + assertEquals(5, (clazz.getMethod("getC").invoke(i) as IntArray)[1]) + assertEquals(6, (clazz.getMethod("getC").invoke(i) as IntArray)[2]) + + assertEquals("$className{a=[1, 2], b=3, c=[4, 5, 6]}", i.toString()) + } + + @Test + @Suppress("UNCHECKED_CAST") + fun `string array`() { + val className = "iEnjoyToast" + val schema = ClassCarpenter.ClassSchema( + "gen.$className", + mapOf("a" to ClassCarpenter.NullableField(Array::class.java))) + + val clazz = cc.build(schema) + + val i = clazz.constructors[0].newInstance(arrayOf("toast", "butter", "jam")) + val arr = clazz.getMethod("getA").invoke(i) as Array + + assertEquals("toast", arr[0]) + assertEquals("butter", arr[1]) + assertEquals("jam", arr[2]) + } + + @Test + @Suppress("UNCHECKED_CAST") + fun `string arrays`() { + val className = "iEnjoyToast" + val schema = ClassCarpenter.ClassSchema( + "gen.$className", + mapOf( + "a" to Array::class.java, + "b" to String::class.java, + "c" to Array::class.java).mapValues { ClassCarpenter.NullableField (it.value) }) + + val clazz = cc.build(schema) + + val i = clazz.constructors[0].newInstance( + arrayOf("bread", "spread", "cheese"), + "and on the side", + arrayOf("some pickles", "some fries")) + + + val arr1 = clazz.getMethod("getA").invoke(i) as Array + val arr2 = clazz.getMethod("getC").invoke(i) as Array + + assertEquals("bread", arr1[0]) + assertEquals("spread", arr1[1]) + assertEquals("cheese", arr1[2]) + assertEquals("and on the side", clazz.getMethod("getB").invoke(i)) + assertEquals("some pickles", arr2[0]) + assertEquals("some fries", arr2[1]) + } } From 65f385953f746072ec501b0067506b897f6c25c2 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Wed, 5 Jul 2017 10:57:18 +0100 Subject: [PATCH 48/97] Changes the name of the addTimeWindow method to setTimeWindow. --- .../core/transactions/TransactionBuilder.kt | 37 ++++++++++--------- .../TransactionSerializationTests.kt | 2 +- .../java/net/corda/docs/FlowCookbookJava.java | 6 ++- .../kotlin/net/corda/docs/FlowCookbook.kt | 6 ++- .../docs/WorkflowTransactionBuildTutorial.kt | 4 +- .../net/corda/contracts/asset/Obligation.kt | 2 +- .../net/corda/flows/TwoPartyDealFlow.kt | 4 +- .../net/corda/flows/TwoPartyTradeFlow.kt | 4 +- .../corda/contracts/CommercialPaperTests.kt | 4 +- .../corda/node/services/NotaryChangeTests.kt | 2 +- .../transactions/NotaryServiceTests.kt | 4 +- .../node/services/vault/VaultQueryTests.kt | 8 ++-- .../kotlin/net/corda/irs/flows/FixingFlow.kt | 4 +- .../kotlin/net/corda/irs/contract/IRSTests.kt | 4 +- .../net/corda/vega/flows/StateRevisionFlow.kt | 2 +- .../net/corda/traderdemo/flow/SellerFlow.kt | 2 +- .../main/kotlin/net/corda/testing/TestDSL.kt | 2 +- .../testing/TransactionDSLInterpreter.kt | 4 +- 18 files changed, 56 insertions(+), 45 deletions(-) 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 abfa261da6..b9d4574ecf 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -40,7 +40,7 @@ open class TransactionBuilder( protected val outputs: MutableList> = arrayListOf(), protected val commands: MutableList = arrayListOf(), protected val signers: MutableSet = mutableSetOf(), - protected var timeWindow: TimeWindow? = null) { + window: TimeWindow? = null) { constructor(type: TransactionType, notary: Party) : this(type, notary, (Strand.currentStrand() as? FlowStateMachine<*>)?.id?.uuid ?: UUID.randomUUID()) /** @@ -55,7 +55,7 @@ open class TransactionBuilder( outputs = ArrayList(outputs), commands = ArrayList(commands), signers = LinkedHashSet(signers), - timeWindow = timeWindow + window = timeWindow ) // DOCSTART 1 @@ -120,24 +120,27 @@ open class TransactionBuilder( fun addCommand(data: CommandData, vararg keys: PublicKey) = addCommand(Command(data, listOf(*keys))) fun addCommand(data: CommandData, keys: List) = addCommand(Command(data, keys)) + var timeWindow: TimeWindow? = window + /** + * Sets the [TimeWindow] for this transaction, replacing the existing [TimeWindow] if there is one. To be valid, the + * transaction must then be signed by the notary service within this window of time. In this way, the notary acts as + * the Timestamp Authority. + */ + set(value) { + check(notary != null) { "Only notarised transactions can have a time-window" } + signers.add(notary!!.owningKey) + field = value + } + /** - * 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 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 + * The [TimeWindow] for the transaction can also be defined as [time] +/- [timeTolerance]. The tolerance should be + * chosen such that your code can finish building the transaction and sending it to the Timestamp Authority 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 addTimeWindow(time: Instant, timeTolerance: Duration) = addTimeWindow(TimeWindow.withTolerance(time, timeTolerance)) - - fun addTimeWindow(timeWindow: TimeWindow) { - check(notary != null) { "Only notarised transactions can have a time-window" } - signers.add(notary!!.owningKey) - this.timeWindow = timeWindow + fun setTimeWindow(time: Instant, timeTolerance: Duration) { + timeWindow = TimeWindow.withTolerance(time, timeTolerance) } // Accessors that yield immutable snapshots. @@ -200,4 +203,4 @@ open class TransactionBuilder( require(commands.any { it.signers.any { sig.by in it.keys } }) { "Signature key doesn't match any command" } sig.verify(toWireTransaction().id) } -} +} \ No newline at end of file diff --git a/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt b/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt index 5b6217ec58..d006cdb134 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt @@ -99,7 +99,7 @@ class TransactionSerializationTests { @Test fun timeWindow() { - tx.addTimeWindow(TEST_TX_TIME, 30.seconds) + tx.setTimeWindow(TEST_TX_TIME, 30.seconds) val ptx = megaCorpServices.signInitialTransaction(tx) val stx = notaryServices.addSignature(ptx) assertEquals(TEST_TX_TIME, stx.tx.timeWindow?.midpoint) diff --git a/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java b/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java index 6392a4e133..68e660d739 100644 --- a/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java +++ b/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java @@ -30,6 +30,7 @@ import net.corda.flows.SignTransactionFlow; import org.bouncycastle.asn1.x500.X500Name; import java.security.PublicKey; +import java.time.Duration; import java.time.Instant; import java.util.List; import java.util.Set; @@ -303,7 +304,10 @@ public class FlowCookbookJava { regTxBuilder.addOutputState(ourOutput); regTxBuilder.addCommand(ourCommand); regTxBuilder.addAttachment(ourAttachment); - regTxBuilder.addTimeWindow(ourTimeWindow); + + // We set the time-window within which the transaction must be notarised using either of: + regTxBuilder.setTimeWindow(ourTimeWindow); + regTxBuilder.setTimeWindow(getServiceHub().getClock().instant(), Duration.ofSeconds(30)); /*----------------------- * TRANSACTION SIGNING * diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt index fa25e494e8..acb355c09c 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt @@ -29,6 +29,7 @@ import net.corda.flows.ResolveTransactionsFlow import net.corda.flows.SignTransactionFlow import org.bouncycastle.asn1.x500.X500Name import java.security.PublicKey +import java.time.Duration import java.time.Instant // We group our two flows inside a singleton object to indicate that they work @@ -286,7 +287,10 @@ object FlowCookbook { regTxBuilder.addOutputState(ourOutput) regTxBuilder.addCommand(ourCommand) regTxBuilder.addAttachment(ourAttachment) - regTxBuilder.addTimeWindow(ourTimeWindow) + + // We set the time-window within which the transaction must be notarised using either of: + regTxBuilder.timeWindow = ourTimeWindow + regTxBuilder.setTimeWindow(serviceHub.clock.instant(), Duration.ofSeconds(30)) /**---------------------- * TRANSACTION SIGNING * diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt index 43c7b5b245..7e66e01fd7 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt @@ -123,7 +123,7 @@ class SubmitTradeApprovalFlow(val tradeId: String, // Create the TransactionBuilder and populate with the new state. val tx = TransactionType.General.Builder(notary) .withItems(tradeProposal, Command(TradeApprovalContract.Commands.Issue(), listOf(tradeProposal.source.owningKey))) - tx.addTimeWindow(serviceHub.clock.instant(), Duration.ofSeconds(60)) + tx.setTimeWindow(serviceHub.clock.instant(), Duration.ofSeconds(60)) // We can automatically sign as there is no untrusted data. val signedTx = serviceHub.signInitialTransaction(tx) // Notarise and distribute. @@ -184,7 +184,7 @@ class SubmitCompletionFlow(val ref: StateRef, val verdict: WorkflowState) : Flow newState, Command(TradeApprovalContract.Commands.Completed(), listOf(serviceHub.myInfo.legalIdentity.owningKey, latestRecord.state.data.source.owningKey))) - tx.addTimeWindow(serviceHub.clock.instant(), Duration.ofSeconds(60)) + tx.setTimeWindow(serviceHub.clock.instant(), Duration.ofSeconds(60)) // We can sign this transaction immediately as we have already checked all the fields and the decision // is ultimately a manual one from the caller. // As a SignedTransaction we can pass the data around certain that it cannot be modified, diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt b/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt index cfd6abdab3..2f5dc79aee 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt @@ -543,7 +543,7 @@ class Obligation

: Contract { } tx.addCommand(Commands.SetLifecycle(lifecycle), partiesUsed.map { it.owningKey }.distinct()) } - tx.addTimeWindow(issuanceDef.dueBefore, issuanceDef.timeTolerance) + tx.setTimeWindow(issuanceDef.dueBefore, issuanceDef.timeTolerance) } /** diff --git a/finance/src/main/kotlin/net/corda/flows/TwoPartyDealFlow.kt b/finance/src/main/kotlin/net/corda/flows/TwoPartyDealFlow.kt index d9ebadad53..8509e55c9b 100644 --- a/finance/src/main/kotlin/net/corda/flows/TwoPartyDealFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/TwoPartyDealFlow.kt @@ -183,9 +183,9 @@ object TwoPartyDealFlow { val deal = handshake.payload.dealBeingOffered val ptx = deal.generateAgreement(handshake.payload.notary) - // And add a request for a time-window: it may be that none of the contracts need this! + // We set the transaction's time-window: it may be that none of the contracts need this! // But it can't hurt to have one. - ptx.addTimeWindow(serviceHub.clock.instant(), 30.seconds) + ptx.setTimeWindow(serviceHub.clock.instant(), 30.seconds) return Pair(ptx, arrayListOf(deal.participants.single { it == serviceHub.myInfo.legalIdentity as AbstractParty }.owningKey)) } } diff --git a/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt b/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt index bb7f4eda86..ebf7c704a3 100644 --- a/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt @@ -195,10 +195,10 @@ object TwoPartyTradeFlow { tx.addOutputState(state, tradeRequest.assetForSale.state.notary) tx.addCommand(command, tradeRequest.assetForSale.state.data.owner.owningKey) - // And add a request for a time-window: it may be that none of the contracts need this! + // We set the transaction's time-window: it may be that none of the contracts need this! // But it can't hurt to have one. val currentTime = serviceHub.clock.instant() - tx.addTimeWindow(currentTime, 30.seconds) + tx.setTimeWindow(currentTime, 30.seconds) return Pair(tx, cashSigningPubKeys) } // DOCEND 1 diff --git a/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt b/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt index caf719047d..404de8f62d 100644 --- a/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt @@ -260,7 +260,7 @@ class CommercialPaperTestsGeneric { val faceValue = 10000.DOLLARS `issued by` DUMMY_CASH_ISSUER val issuance = bigCorpServices.myInfo.legalIdentity.ref(1) val issueBuilder = CommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, DUMMY_NOTARY) - issueBuilder.addTimeWindow(TEST_TX_TIME, 30.seconds) + issueBuilder.setTimeWindow(TEST_TX_TIME, 30.seconds) val issuePtx = bigCorpServices.signInitialTransaction(issueBuilder) val issueTx = notaryServices.addSignature(issuePtx) @@ -289,7 +289,7 @@ class CommercialPaperTestsGeneric { databaseBigCorp.transaction { fun makeRedeemTX(time: Instant): Pair { val builder = TransactionType.General.Builder(DUMMY_NOTARY) - builder.addTimeWindow(time, 30.seconds) + builder.setTimeWindow(time, 30.seconds) CommercialPaper().generateRedeem(builder, moveTX.tx.outRef(1), bigCorpVaultService) val ptx = aliceServices.signInitialTransaction(builder) val ptx2 = bigCorpServices.addSignature(ptx) diff --git a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt index 4ff4962f76..346f124aaa 100644 --- a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt @@ -172,7 +172,7 @@ fun issueMultiPartyState(nodeA: AbstractNode, nodeB: AbstractNode, notaryNode: A fun issueInvalidState(node: AbstractNode, notary: Party): StateAndRef<*> { val tx = DummyContract.generateInitial(Random().nextInt(), notary, node.info.legalIdentity.ref(0)) - tx.addTimeWindow(Instant.now(), 30.seconds) + tx.setTimeWindow(Instant.now(), 30.seconds) val stx = node.services.signInitialTransaction(tx) node.services.recordTransactions(stx) return StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0)) diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt index deaa8bc278..8259ab35c8 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt @@ -43,7 +43,7 @@ class NotaryServiceTests { val stx = run { val inputState = issueState(clientNode) val tx = TransactionType.General.Builder(notaryNode.info.notaryIdentity).withItems(inputState) - tx.addTimeWindow(Instant.now(), 30.seconds) + tx.setTimeWindow(Instant.now(), 30.seconds) clientNode.services.signInitialTransaction(tx) } @@ -68,7 +68,7 @@ class NotaryServiceTests { val stx = run { val inputState = issueState(clientNode) val tx = TransactionType.General.Builder(notaryNode.info.notaryIdentity).withItems(inputState) - tx.addTimeWindow(Instant.now().plusSeconds(3600), 30.seconds) + tx.setTimeWindow(Instant.now().plusSeconds(3600), 30.seconds) clientNode.services.signInitialTransaction(tx) } diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index 20a9e8d938..914a550e79 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -1241,7 +1241,7 @@ class VaultQueryTests { val faceValue = 10000.DOLLARS `issued by` DUMMY_CASH_ISSUER val commercialPaper = CommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply { - addTimeWindow(TEST_TX_TIME, 30.seconds) + setTimeWindow(TEST_TX_TIME, 30.seconds) signWith(MEGA_CORP_KEY) signWith(DUMMY_NOTARY_KEY) }.toSignedTransaction() @@ -1251,7 +1251,7 @@ class VaultQueryTests { val faceValue2 = 10000.POUNDS `issued by` DUMMY_CASH_ISSUER val commercialPaper2 = CommercialPaper().generateIssue(issuance, faceValue2, TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply { - addTimeWindow(TEST_TX_TIME, 30.seconds) + setTimeWindow(TEST_TX_TIME, 30.seconds) signWith(MEGA_CORP_KEY) signWith(DUMMY_NOTARY_KEY) }.toSignedTransaction() @@ -1278,7 +1278,7 @@ class VaultQueryTests { val faceValue = 10000.DOLLARS `issued by` DUMMY_CASH_ISSUER val commercialPaper = CommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply { - addTimeWindow(TEST_TX_TIME, 30.seconds) + setTimeWindow(TEST_TX_TIME, 30.seconds) signWith(MEGA_CORP_KEY) signWith(DUMMY_NOTARY_KEY) }.toSignedTransaction() @@ -1288,7 +1288,7 @@ class VaultQueryTests { val faceValue2 = 5000.POUNDS `issued by` DUMMY_CASH_ISSUER val commercialPaper2 = CommercialPaper().generateIssue(issuance, faceValue2, TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply { - addTimeWindow(TEST_TX_TIME, 30.seconds) + setTimeWindow(TEST_TX_TIME, 30.seconds) signWith(MEGA_CORP_KEY) signWith(DUMMY_NOTARY_KEY) }.toSignedTransaction() diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt index f63e3c3217..57110870aa 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt @@ -72,9 +72,9 @@ object FixingFlow { override fun beforeSigning(fix: Fix) { newDeal.generateFix(ptx, StateAndRef(txState, handshake.payload.ref), fix) - // And add a request for a time-window: it may be that none of the contracts need this! + // We set the transaction's time-window: it may be that none of the contracts need this! // But it can't hurt to have one. - ptx.addTimeWindow(serviceHub.clock.instant(), 30.seconds) + ptx.setTimeWindow(serviceHub.clock.instant(), 30.seconds) } @Suspendable diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/contract/IRSTests.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/contract/IRSTests.kt index 2845d4f146..e1c68fab13 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/contract/IRSTests.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/contract/IRSTests.kt @@ -227,7 +227,7 @@ class IRSTests { calculation = dummyIRS.calculation, common = dummyIRS.common, notary = DUMMY_NOTARY).apply { - addTimeWindow(TEST_TX_TIME, 30.seconds) + setTimeWindow(TEST_TX_TIME, 30.seconds) } val ptx1 = megaCorpServices.signInitialTransaction(gtx) val ptx2 = miniCorpServices.addSignature(ptx1) @@ -311,7 +311,7 @@ class IRSTests { val tx = TransactionType.General.Builder(DUMMY_NOTARY) val fixing = Fix(nextFix, "0.052".percent.value) InterestRateSwap().generateFix(tx, previousTXN.tx.outRef(0), fixing) - tx.addTimeWindow(TEST_TX_TIME, 30.seconds) + tx.setTimeWindow(TEST_TX_TIME, 30.seconds) val ptx1 = megaCorpServices.signInitialTransaction(tx) val ptx2 = miniCorpServices.addSignature(ptx1) notaryServices.addSignature(ptx2) diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/StateRevisionFlow.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/StateRevisionFlow.kt index 5cda09ea03..c9d56ae403 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/StateRevisionFlow.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/StateRevisionFlow.kt @@ -20,7 +20,7 @@ object StateRevisionFlow { override fun assembleTx(): AbstractStateReplacementFlow.UpgradeTx { val state = originalState.state.data val tx = state.generateRevision(originalState.state.notary, originalState, modification) - tx.addTimeWindow(serviceHub.clock.instant(), 30.seconds) + tx.setTimeWindow(serviceHub.clock.instant(), 30.seconds) val stx = serviceHub.signInitialTransaction(tx) val participantKeys = state.participants.map { it.owningKey } diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt index 6a096caf0d..e46b4073ec 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt @@ -82,7 +82,7 @@ class SellerFlow(val otherParty: Party, tx.addAttachment(serviceHub.attachments.openAttachment(PROSPECTUS_HASH)!!.id) // Requesting a time-window to be set, all CP must have a validation window. - tx.addTimeWindow(Instant.now(), 30.seconds) + tx.setTimeWindow(Instant.now(), 30.seconds) // Sign it as ourselves. val stx = serviceHub.signInitialTransaction(tx) diff --git a/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt b/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt index a12f813779..faaefa8653 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt @@ -145,7 +145,7 @@ data class TestTransactionDSLInterpreter private constructor( } override fun timeWindow(data: TimeWindow) { - transactionBuilder.addTimeWindow(data) + transactionBuilder.timeWindow = data } override fun tweak( diff --git a/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt b/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt index 4e8d63212a..d3ea0a1788 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt @@ -52,7 +52,7 @@ interface TransactionDSLInterpreter : Verifies, OutputStateLookup { fun _command(signers: List, commandData: CommandData) /** - * Adds a time-window to the transaction. + * Sets the time-window of the transaction. * @param data the [TimeWindow] (validation window). */ fun timeWindow(data: TimeWindow) @@ -116,7 +116,7 @@ class TransactionDSL(val interpreter: T) : Tr fun command(signer: PublicKey, commandData: CommandData) = _command(listOf(signer), commandData) /** - * Adds a [TimeWindow] command to the transaction. + * Sets the [TimeWindow] of the transaction. * @param time The [Instant] of the [TimeWindow]. * @param tolerance The tolerance of the [TimeWindow]. */ From 7d8d17ac080e4bd326dd0031acc040e74c508a01 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Wed, 5 Jul 2017 11:05:27 +0100 Subject: [PATCH 49/97] Small tidyup --- .../core/serialization/carpenter/ClassCarpenter.kt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/serialization/carpenter/ClassCarpenter.kt b/core/src/main/kotlin/net/corda/core/serialization/carpenter/ClassCarpenter.kt index 46bbafff3b..fde7632034 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/carpenter/ClassCarpenter.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/carpenter/ClassCarpenter.kt @@ -76,7 +76,7 @@ class ClassCarpenter { class NullablePrimitive(msg: String) : RuntimeException(msg) abstract class Field(val field: Class) { - val unsetName = "Unset" + protected val unsetName = "Unset" var name: String = unsetName abstract val nullabilityAnnotation: String @@ -96,8 +96,6 @@ class ClassCarpenter { mv.visitAnnotation(nullabilityAnnotation, false) } - abstract fun copy(name: String, field: Class): Field - abstract fun nullTest(mv: MethodVisitor, slot: Int) fun visitParameter(mv: MethodVisitor, idx: Int) { with(mv) { visitParameter(name, 0) @@ -106,6 +104,9 @@ class ClassCarpenter { } } } + + abstract fun copy(name: String, field: Class): Field + abstract fun nullTest(mv: MethodVisitor, slot: Int) } class NonNullableField(field: Class) : Field(field) { @@ -341,9 +342,8 @@ class ClassCarpenter { private fun ClassWriter.generateAbstractGetters(schema: Schema) { for ((name, field) in schema.fields) { - val descriptor = field.descriptor val opcodes = ACC_ABSTRACT + ACC_PUBLIC - with(visitMethod(opcodes, "get" + name.capitalize(), "()" + descriptor, null, null)) { + with(visitMethod(opcodes, "get" + name.capitalize(), "()${field.descriptor}", null, null)) { // abstract method doesn't have any implementation so just end visitEnd() } @@ -378,7 +378,6 @@ class ClassCarpenter { // Assign the fields from parameters. var slot = 1 + superclassFields.size - for ((name, field) in schema.fields.entries) { field.nullTest(this, slot) From 3176ecfecfcb3bdbd65649aaddc578226332d9d0 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Wed, 5 Jul 2017 11:39:08 +0100 Subject: [PATCH 50/97] Clean up transaction key flow * Identities returned from TxKeyFlow were backwards, meaning keys were incorrectly assigned to the remote and local identities. Added unit test covering this case and corrected the flow logic. * Rename TxKeyFlow to TransactionKeyFlow * Correct registration of transaction key flows * Move TransactionKeyFlow.Provider into CoreFlowHandlers * Move TransactionKeyFlow.Request up to the top level class instead of being a class within an object. * Remove AbstractIdentityFlow and move the validation logic into individual flows to make it clearer that it's registering the received identities. * Cash flows now return the recipient identity instead of full identity lookup, as this is what the caller actually needs and simplifies a lot of cases. --- .../net/corda/flows/TransactionKeyFlow.kt | 50 ++++++++++ .../main/kotlin/net/corda/flows/TxKeyFlow.kt | 91 ------------------- ...lowTests.kt => TransactionKeyFlowTests.kt} | 16 +++- .../net/corda/flows/AbstractCashFlow.kt | 9 +- .../kotlin/net/corda/flows/CashExitFlow.kt | 2 +- .../kotlin/net/corda/flows/CashIssueFlow.kt | 12 +-- .../kotlin/net/corda/flows/CashPaymentFlow.kt | 12 +-- .../main/kotlin/net/corda/flows/IssuerFlow.kt | 10 +- .../net/corda/flows/CashPaymentFlowTests.kt | 5 +- .../kotlin/net/corda/flows/IssuerFlowTest.kt | 1 - .../net/corda/node/internal/AbstractNode.kt | 3 +- .../corda/node/services/CoreFlowHandlers.kt | 22 +++++ .../network/InMemoryIdentityServiceTests.kt | 1 - .../kotlin/net/corda/testing/node/MockNode.kt | 4 +- .../net/corda/explorer/ExplorerSimulation.kt | 1 - 15 files changed, 106 insertions(+), 133 deletions(-) create mode 100644 core/src/main/kotlin/net/corda/flows/TransactionKeyFlow.kt delete mode 100644 core/src/main/kotlin/net/corda/flows/TxKeyFlow.kt rename core/src/test/kotlin/net/corda/flows/{TxKeyFlowTests.kt => TransactionKeyFlowTests.kt} (70%) diff --git a/core/src/main/kotlin/net/corda/flows/TransactionKeyFlow.kt b/core/src/main/kotlin/net/corda/flows/TransactionKeyFlow.kt new file mode 100644 index 0000000000..1989ef5d82 --- /dev/null +++ b/core/src/main/kotlin/net/corda/flows/TransactionKeyFlow.kt @@ -0,0 +1,50 @@ +package net.corda.flows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.Party +import net.corda.core.utilities.ProgressTracker +import net.corda.core.utilities.unwrap + +/** + * 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. + */ +@StartableByRPC +@InitiatingFlow +class TransactionKeyFlow(val otherSide: Party, + val revocationEnabled: Boolean, + override val progressTracker: ProgressTracker) : FlowLogic>() { + constructor(otherSide: Party) : this(otherSide, false, tracker()) + + companion object { + object AWAITING_KEY : ProgressTracker.Step("Awaiting key") + + fun tracker() = ProgressTracker(AWAITING_KEY) + fun validateIdentity(otherSide: Party, anonymousOtherSide: AnonymisedIdentity): AnonymisedIdentity { + require(anonymousOtherSide.certificate.subject == otherSide.name) + return anonymousOtherSide + } + } + + @Suspendable + override fun call(): LinkedHashMap { + progressTracker.currentStep = AWAITING_KEY + val legalIdentityAnonymous = serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, revocationEnabled) + serviceHub.identityService.registerAnonymousIdentity(legalIdentityAnonymous.identity, serviceHub.myInfo.legalIdentity, legalIdentityAnonymous.certPath) + + // Special case that if we're both parties, a single identity is generated + val identities = LinkedHashMap() + if (otherSide == serviceHub.myInfo.legalIdentity) { + identities.put(otherSide, legalIdentityAnonymous) + } else { + val otherSideAnonymous = sendAndReceive(otherSide, legalIdentityAnonymous).unwrap { validateIdentity(otherSide, it) } + identities.put(serviceHub.myInfo.legalIdentity, legalIdentityAnonymous) + identities.put(otherSide, otherSideAnonymous) + } + return identities + } + +} diff --git a/core/src/main/kotlin/net/corda/flows/TxKeyFlow.kt b/core/src/main/kotlin/net/corda/flows/TxKeyFlow.kt deleted file mode 100644 index c3f46e69eb..0000000000 --- a/core/src/main/kotlin/net/corda/flows/TxKeyFlow.kt +++ /dev/null @@ -1,91 +0,0 @@ -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.Party -import net.corda.core.serialization.CordaSerializable -import net.corda.core.utilities.ProgressTracker -import net.corda.core.utilities.unwrap - -/** - * 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: AnonymisedIdentity): AnonymisedIdentity { - val (certPath, theirCert, txIdentity) = untrustedIdentity - if (theirCert.subject == otherSide.name) { - serviceHub.identityService.registerAnonymousIdentity(txIdentity, otherSide, certPath) - return AnonymisedIdentity(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(otherSide, false) { - constructor(otherSide: Party) : this(otherSide, tracker()) - companion object { - object AWAITING_KEY : ProgressTracker.Step("Awaiting key") - - fun tracker() = ProgressTracker(AWAITING_KEY) - } - - @Suspendable - override fun call(): TxIdentities { - progressTracker.currentStep = AWAITING_KEY - val myIdentity = serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, revocationEnabled) - serviceHub.identityService.registerAnonymousIdentity(myIdentity.identity, serviceHub.myInfo.legalIdentity, myIdentity.certPath) - - // Special case that if we're both parties, a single identity is generated - return if (otherSide == serviceHub.myInfo.legalIdentity) { - TxIdentities(Pair(otherSide, myIdentity)) - } else { - val theirIdentity = receive(otherSide).unwrap { validateIdentity(it) } - send(otherSide, myIdentity) - TxIdentities(Pair(otherSide, myIdentity), - Pair(serviceHub.myInfo.legalIdentity, theirIdentity)) - } - } - } - - /** - * Flow which waits for a key request from a counterparty, generates a new key and then returns it to the - * counterparty and as the result from the flow. - */ - @InitiatedBy(Requester::class) - class Provider(otherSide: Party) : AbstractIdentityFlow(otherSide, false) { - companion object { - object SENDING_KEY : ProgressTracker.Step("Sending key") - } - - override val progressTracker: ProgressTracker = ProgressTracker(SENDING_KEY) - - @Suspendable - override fun call(): TxIdentities { - val revocationEnabled = false - progressTracker.currentStep = SENDING_KEY - val myIdentity = serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, revocationEnabled) - send(otherSide, myIdentity) - val theirIdentity = receive(otherSide).unwrap { validateIdentity(it) } - return TxIdentities(Pair(otherSide, myIdentity), - Pair(serviceHub.myInfo.legalIdentity, theirIdentity)) - } - } - - @CordaSerializable - data class TxIdentities(val identities: List>) { - constructor(vararg identities: Pair) : this(identities.toList()) - init { - require(identities.size == identities.map { it.first }.toSet().size) { "Identities must be unique: ${identities.map { it.first }}" } - } - fun forParty(party: Party): AnonymisedIdentity = identities.single { it.first == party }.second - fun toMap(): Map = this.identities.toMap() - } -} diff --git a/core/src/test/kotlin/net/corda/flows/TxKeyFlowTests.kt b/core/src/test/kotlin/net/corda/flows/TransactionKeyFlowTests.kt similarity index 70% rename from core/src/test/kotlin/net/corda/flows/TxKeyFlowTests.kt rename to core/src/test/kotlin/net/corda/flows/TransactionKeyFlowTests.kt index 942ca591f9..ce76532392 100644 --- a/core/src/test/kotlin/net/corda/flows/TxKeyFlowTests.kt +++ b/core/src/test/kotlin/net/corda/flows/TransactionKeyFlowTests.kt @@ -10,9 +10,11 @@ import net.corda.testing.node.MockNetwork import org.junit.Before import org.junit.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertNotEquals +import kotlin.test.assertTrue -class TxKeyFlowTests { +class TransactionKeyFlowTests { lateinit var mockNet: MockNetwork @Before @@ -37,7 +39,7 @@ class TxKeyFlowTests { bobNode.services.identityService.registerIdentity(notaryNode.info.legalIdentityAndCert) // Run the flows - val requesterFlow = aliceNode.services.startFlow(TxKeyFlow.Requester(bob)) + val requesterFlow = aliceNode.services.startFlow(TransactionKeyFlow(bob)) // Get the results val actual: Map = requesterFlow.resultFuture.getOrThrow().toMap() @@ -47,5 +49,15 @@ class TxKeyFlowTests { val bobAnonymousIdentity = actual[bob] ?: throw IllegalStateException() assertNotEquals(alice, aliceAnonymousIdentity.identity) assertNotEquals(bob, bobAnonymousIdentity.identity) + + // Verify that the anonymous identities look sane + assertEquals(alice.name, aliceAnonymousIdentity.certificate.subject) + assertEquals(bob.name, bobAnonymousIdentity.certificate.subject) + + // Verify that the nodes have the right anonymous identities + assertTrue { aliceAnonymousIdentity.identity.owningKey in aliceNode.services.keyManagementService.keys } + assertTrue { bobAnonymousIdentity.identity.owningKey in bobNode.services.keyManagementService.keys } + assertFalse { aliceAnonymousIdentity.identity.owningKey in bobNode.services.keyManagementService.keys } + assertFalse { bobAnonymousIdentity.identity.owningKey in aliceNode.services.keyManagementService.keys } } } diff --git a/finance/src/main/kotlin/net/corda/flows/AbstractCashFlow.kt b/finance/src/main/kotlin/net/corda/flows/AbstractCashFlow.kt index a744a9cdee..52bef42f21 100644 --- a/finance/src/main/kotlin/net/corda/flows/AbstractCashFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/AbstractCashFlow.kt @@ -3,9 +3,8 @@ package net.corda.flows import co.paralleluniverse.fibers.Suspendable import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic -import net.corda.core.identity.AnonymousParty +import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party -import net.corda.core.identity.PartyAndCertificate import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.ProgressTracker @@ -37,10 +36,12 @@ abstract class AbstractCashFlow(override val progressTracker: ProgressTracker * Specialised flows for unit tests differ from this. * * @param stx the signed transaction. - * @param identities a mapping from the original identities of the parties to the anonymised equivalents. + * @param recipient the identity used for the other side of the transaction, where applicable (i.e. this is + * null for exit transactions). For anonymous transactions this is the confidential identity generated for the + * transaction, otherwise this is the well known identity. */ @CordaSerializable - data class Result(val stx: SignedTransaction, val identities: TxKeyFlow.TxIdentities) + data class Result(val stx: SignedTransaction, val recipient: AbstractParty?) } class CashException(message: String, cause: Throwable) : FlowException(message, cause) \ No newline at end of file diff --git a/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt b/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt index 7477364082..a80bf0ea8e 100644 --- a/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt @@ -70,6 +70,6 @@ class CashExitFlow(val amount: Amount, val issueRef: OpaqueBytes, prog // Commit the transaction progressTracker.currentStep = FINALISING_TX finaliseTx(participants, tx, "Unable to notarise exit") - return Result(tx, TxKeyFlow.TxIdentities()) + return Result(tx, null) } } diff --git a/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt b/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt index 7fa563e6d8..3147960163 100644 --- a/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt @@ -41,15 +41,11 @@ class CashIssueFlow(val amount: Amount, override fun call(): AbstractCashFlow.Result { progressTracker.currentStep = GENERATING_ID val txIdentities = if (anonymous) { - subFlow(TxKeyFlow.Requester(recipient)) + subFlow(TransactionKeyFlow(recipient)) } else { - TxKeyFlow.TxIdentities(emptyList()) - } - val anonymousRecipient = if (anonymous) { - txIdentities.forParty(recipient).identity - } else { - recipient + emptyMap() } + val anonymousRecipient = txIdentities.get(recipient)?.identity ?: recipient progressTracker.currentStep = GENERATING_TX val builder: TransactionBuilder = TransactionType.General.Builder(notary = notary) val issuer = serviceHub.myInfo.legalIdentity.ref(issueRef) @@ -58,6 +54,6 @@ class CashIssueFlow(val amount: Amount, val tx = serviceHub.signInitialTransaction(builder, signers) progressTracker.currentStep = FINALISING_TX subFlow(FinalityFlow(tx)) - return Result(tx, txIdentities) + return Result(tx, anonymousRecipient) } } diff --git a/finance/src/main/kotlin/net/corda/flows/CashPaymentFlow.kt b/finance/src/main/kotlin/net/corda/flows/CashPaymentFlow.kt index 2bc40307c6..0567e2c77e 100644 --- a/finance/src/main/kotlin/net/corda/flows/CashPaymentFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/CashPaymentFlow.kt @@ -35,15 +35,11 @@ open class CashPaymentFlow( override fun call(): AbstractCashFlow.Result { progressTracker.currentStep = GENERATING_ID val txIdentities = if (anonymous) { - subFlow(TxKeyFlow.Requester(recipient)) + subFlow(TransactionKeyFlow(recipient)) } else { - TxKeyFlow.TxIdentities(emptyList()) - } - val anonymousRecipient = if (anonymous) { - txIdentities.forParty(recipient).identity - } else { - recipient + emptyMap() } + val anonymousRecipient = txIdentities.get(recipient)?.identity ?: recipient progressTracker.currentStep = GENERATING_TX val builder: TransactionBuilder = TransactionType.General.Builder(null as Party?) // TODO: Have some way of restricting this to states the caller controls @@ -62,6 +58,6 @@ open class CashPaymentFlow( progressTracker.currentStep = FINALISING_TX finaliseTx(setOf(recipient), tx, "Unable to notarise spend") - return Result(tx, txIdentities) + return Result(tx, anonymousRecipient) } } \ No newline at end of file diff --git a/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt b/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt index af01f2c069..25fec82ee8 100644 --- a/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt @@ -4,7 +4,6 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.asset.Cash import net.corda.core.contracts.* import net.corda.core.flows.* -import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.OpaqueBytes @@ -47,17 +46,12 @@ object IssuerFlow { val issueRequest = IssuanceRequestState(amount, issueToParty, issueToPartyRef, anonymous) return sendAndReceive(issuerBankParty, issueRequest).unwrap { res -> val tx = res.stx.tx - val recipient = if (anonymous) { - res.identities.forParty(issueToParty).identity - } else { - issueToParty - } val expectedAmount = Amount(amount.quantity, Issued(issuerBankParty.ref(issueToPartyRef), amount.token)) val cashOutputs = tx.outputs .map { it.data} .filterIsInstance() - .filter { state -> state.owner == recipient } - require(cashOutputs.size == 1) { "Require a single cash output paying $recipient, found ${tx.outputs}" } + .filter { state -> state.owner == res.recipient } + require(cashOutputs.size == 1) { "Require a single cash output paying ${res.recipient}, found ${tx.outputs}" } require(cashOutputs.single().amount == expectedAmount) { "Require payment of $expectedAmount"} res } diff --git a/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt b/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt index b7af9399ef..0cc5c91dc1 100644 --- a/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt @@ -32,7 +32,6 @@ class CashPaymentFlowTests { notary = notaryNode.info.notaryIdentity bankOfCorda = bankOfCordaNode.info.legalIdentity - notaryNode.registerInitiatedFlow(TxKeyFlow.Provider::class.java) notaryNode.services.identityService.registerIdentity(bankOfCordaNode.info.legalIdentityAndCert) bankOfCordaNode.services.identityService.registerIdentity(notaryNode.info.legalIdentityAndCert) val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, ref, @@ -55,9 +54,9 @@ class CashPaymentFlowTests { val future = bankOfCordaNode.services.startFlow(CashPaymentFlow(expectedPayment, payTo)).resultFuture mockNet.runNetwork() - val (paymentTx, identities) = future.getOrThrow() + val (paymentTx, receipient) = future.getOrThrow() val states = paymentTx.tx.outputs.map { it.data }.filterIsInstance() - val paymentState: Cash.State = states.single { it.owner == identities.forParty(payTo).identity } + val paymentState: Cash.State = states.single { it.owner == receipient } val changeState: Cash.State = states.single { it != paymentState } assertEquals(expectedChange.`issued by`(bankOfCorda.ref(ref)), changeState.amount) assertEquals(expectedPayment.`issued by`(bankOfCorda.ref(ref)), paymentState.amount) diff --git a/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt b/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt index 32217fde9a..7d1714933a 100644 --- a/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt +++ b/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt @@ -43,7 +43,6 @@ class IssuerFlowTest { nodes.forEach { node -> nodes.map { it.info.legalIdentityAndCert }.forEach(node.services.identityService::registerIdentity) - node.registerInitiatedFlow(TxKeyFlow.Provider::class.java) } } diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 9a34e809d0..a6202cedd9 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -212,8 +212,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, findRPCFlows(scanResult) } - // TODO: Investigate having class path scanning find this flow - registerInitiatedFlow(TxKeyFlow.Provider::class.java) // TODO Remove this once the cash stuff is in its own CorDapp registerInitiatedFlow(IssuerFlow.Issuer::class.java) @@ -413,6 +411,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, installCoreFlow(BroadcastTransactionFlow::class) { otherParty, _ -> NotifyTransactionHandler(otherParty) } installCoreFlow(NotaryChangeFlow::class) { otherParty, _ -> NotaryChangeHandler(otherParty) } installCoreFlow(ContractUpgradeFlow::class) { otherParty, _ -> ContractUpgradeHandler(otherParty) } + installCoreFlow(TransactionKeyFlow::class) { otherParty, _ -> TransactionKeyHandler(otherParty) } } /** diff --git a/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt b/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt index 670834ff08..3359d55e58 100644 --- a/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt +++ b/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt @@ -10,6 +10,7 @@ import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic import net.corda.core.identity.Party import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.unwrap import net.corda.flows.* @@ -122,3 +123,24 @@ class ContractUpgradeHandler(otherSide: Party) : AbstractStateReplacementFlow.Ac ContractUpgradeFlow.verify(oldStateAndRef.state.data, expectedTx.outRef(0).state.data, expectedTx.commands.single()) } } + +class TransactionKeyHandler(val otherSide: Party, val revocationEnabled: Boolean) : FlowLogic() { + constructor(otherSide: Party) : this(otherSide, false) + companion object { + object SENDING_KEY : ProgressTracker.Step("Sending key") + } + + override val progressTracker: ProgressTracker = ProgressTracker(SENDING_KEY) + + @Suspendable + override fun call(): Unit { + val revocationEnabled = false + progressTracker.currentStep = SENDING_KEY + val legalIdentityAnonymous = serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, revocationEnabled) + val otherSideAnonymous = sendAndReceive(otherSide, legalIdentityAnonymous).unwrap { TransactionKeyFlow.validateIdentity(otherSide, it) } + val (certPath, theirCert, txIdentity) = otherSideAnonymous + // Validate then store their identity so that we can prove the key in the transaction is owned by the + // counterparty. + serviceHub.identityService.registerAnonymousIdentity(txIdentity, otherSide, certPath) + } +} \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt index 55f7f6d6aa..ceaa7b36d9 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt @@ -7,7 +7,6 @@ import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.services.IdentityService import net.corda.core.utilities.* import net.corda.flows.AnonymisedIdentity -import net.corda.flows.TxKeyFlow import net.corda.node.services.identity.InMemoryIdentityService import net.corda.testing.ALICE_PUBKEY import net.corda.testing.BOB_PUBKEY diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt index 2598929ba5..0d28e98807 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -23,7 +23,7 @@ import net.corda.core.node.services.ServiceInfo import net.corda.core.utilities.DUMMY_NOTARY_KEY import net.corda.core.utilities.getTestPartyAndCertificate import net.corda.core.utilities.loggerFor -import net.corda.flows.TxKeyFlow +import net.corda.flows.TransactionKeyFlow import net.corda.node.internal.AbstractNode import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.identity.InMemoryIdentityService @@ -301,8 +301,6 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, val node = nodeFactory.create(config, this, networkMapAddress, advertisedServices.toSet(), id, overrideServices, entropyRoot) if (start) { node.setup().start() - // Register flows that are normally found via plugins - node.registerInitiatedFlow(TxKeyFlow.Provider::class.java) if (threadPerNode && networkMapAddress != null) node.networkMapRegistrationFuture.getOrThrow() // Block and wait for the node to register in the net map. } diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt index 7ea256d5cd..6a310ab4a9 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt @@ -18,7 +18,6 @@ import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceType import net.corda.core.serialization.OpaqueBytes import net.corda.core.success -import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.ALICE import net.corda.core.utilities.BOB import net.corda.core.utilities.DUMMY_NOTARY From 2973755bc83280266faf9dd59a11ac62b61ccf4a Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Tue, 4 Jul 2017 13:56:51 +0100 Subject: [PATCH 51/97] Moved TestConstants.kt from core into test-utils --- .../corda/client/jfx/NodeMonitorModelTest.kt | 8 +-- .../corda/client/rpc/CordaRPCClientTest.kt | 2 +- .../kotlin/rpc/StandaloneCordaRPClientTest.kt | 6 +- .../core/contracts/DummyContractV2Tests.kt | 4 +- .../contracts/TransactionGraphSearchTests.kt | 4 +- .../corda/core/contracts/TransactionTests.kt | 2 +- .../core/crypto/PartialMerkleTreeTest.kt | 6 +- .../core/flows/ResolveTransactionsFlowTest.kt | 2 +- .../net/corda/core/identity/PartyTest.kt | 2 +- .../core/node/AttachmentClassLoaderTests.kt | 2 +- .../net/corda/core/node/VaultUpdateTests.kt | 2 +- .../net/corda/core/serialization/KryoTests.kt | 4 +- .../TransactionSerializationTests.kt | 1 - .../corda/flows/TransactionKeyFlowTests.kt | 6 +- docs/source/changelog.rst | 3 + .../corda/docs/IntegrationTestingTutorial.kt | 6 +- .../java/net/corda/docs/FlowCookbookJava.java | 2 +- .../net/corda/docs/ClientRpcTutorial.kt | 4 +- .../kotlin/net/corda/docs/FlowCookbook.kt | 2 +- .../docs/FxTransactionBuildTutorialTest.kt | 4 +- .../WorkflowTransactionBuildTutorialTest.kt | 4 +- .../net/corda/contracts/universal/Cap.kt | 2 +- .../net/corda/contracts/universal/Caplet.kt | 2 +- .../contracts/universal/ContractDefinition.kt | 2 +- .../contracts/universal/FXFwdTimeOption.kt | 2 +- .../net/corda/contracts/universal/FXSwap.kt | 2 +- .../net/corda/contracts/universal/IRS.kt | 2 +- .../corda/contracts/universal/RollOutTests.kt | 2 +- .../net/corda/contracts/universal/Swaption.kt | 2 +- .../contracts/universal/ZeroCouponBond.kt | 2 +- .../net/corda/contracts/asset/Obligation.kt | 7 -- .../corda/contracts/asset/CashTestsJava.java | 4 +- .../corda/contracts/CommercialPaperTests.kt | 3 +- .../net/corda/contracts/asset/CashTests.kt | 2 +- .../corda/contracts/asset/ObligationTests.kt | 13 +++- .../kotlin/net/corda/flows/IssuerFlowTest.kt | 4 +- node-schemas/build.gradle | 6 +- .../services/vault/schemas/VaultSchemaTest.kt | 8 +-- .../kotlin/net/corda/node/BootTests.kt | 2 +- .../corda/node/CordappScanningDriverTest.kt | 4 +- .../node/services/DistributedServiceTests.kt | 4 +- .../node/services/RaftNotaryServiceTests.kt | 2 +- .../statemachine/FlowVersioningTest.kt | 4 +- .../services/messaging/MQSecurityTest.kt | 4 +- .../services/messaging/P2PMessagingTest.kt | 4 +- .../services/messaging/P2PSecurityTest.kt | 5 +- .../net/corda/node/internal/AbstractNode.kt | 18 +++-- .../node/CordappScanningNodeProcessTest.kt | 4 +- .../services/vault/VaultQueryJavaTests.java | 65 ++++++++++--------- .../net/corda/node/InteractiveShellTest.kt | 2 +- .../node/messaging/TwoPartyTradeFlowTests.kt | 2 +- .../corda/node/services/NotaryChangeTests.kt | 4 +- .../config/FullNodeConfigurationTest.kt | 2 +- .../database/HibernateConfigurationTest.kt | 16 ++--- .../database/RequeryConfigurationTest.kt | 4 +- .../events/NodeSchedulerServiceTest.kt | 6 +- .../services/events/ScheduledFlowTests.kt | 2 +- .../messaging/ArtemisMessagingTests.kt | 2 +- .../network/AbstractNetworkMapServiceTest.kt | 8 +-- .../network/InMemoryIdentityServiceTests.kt | 8 +-- .../network/InMemoryNetworkMapCacheTest.kt | 4 +- .../persistence/DBTransactionStorageTests.kt | 2 +- .../persistence/DataVendingServiceTests.kt | 2 +- .../transactions/NotaryServiceTests.kt | 2 +- .../ValidatingNotaryServiceTests.kt | 2 +- .../services/vault/NodeVaultServiceTest.kt | 4 +- .../node/services/vault/VaultQueryTests.kt | 8 +-- .../node/services/vault/VaultWithCashTest.kt | 12 ++-- .../NetworkisRegistrationHelperTest.kt | 2 +- .../attachmentdemo/AttachmentDemoTest.kt | 6 +- .../corda/attachmentdemo/AttachmentDemo.kt | 2 + .../kotlin/net/corda/attachmentdemo/Main.kt | 6 +- .../net/corda/bank/BankOfCordaDriver.kt | 3 +- .../kotlin/net/corda/irs/IRSDemoTest.kt | 6 +- .../src/test/kotlin/net/corda/irs/Main.kt | 6 +- .../corda/irs/api/NodeInterestRatesTest.kt | 4 +- .../kotlin/net/corda/irs/contract/IRSTests.kt | 6 +- .../corda/netmap/simulation/IRSSimulation.kt | 2 +- .../net/corda/netmap/simulation/Simulation.kt | 6 +- .../net/corda/notarydemo/BFTNotaryCordform.kt | 4 +- .../kotlin/net/corda/notarydemo/Notarise.kt | 2 +- .../corda/notarydemo/RaftNotaryCordform.kt | 6 +- .../corda/notarydemo/SingleNotaryCordform.kt | 6 +- .../net/corda/vega/SimmValuationTest.kt | 6 +- .../kotlin/net/corda/vega/api/PortfolioApi.kt | 18 ++--- .../src/test/kotlin/net/corda/vega/Main.kt | 8 +-- .../net/corda/traderdemo/TraderDemoTest.kt | 6 +- .../kotlin/net/corda/traderdemo/TraderDemo.kt | 4 +- .../corda/traderdemo/TraderDemoClientApi.kt | 2 +- .../test/kotlin/net/corda/traderdemo/Main.kt | 6 +- .../net/corda/smoketesting/NodeConfig.kt | 22 +++---- .../net/corda/testing/driver/DriverTests.kt | 7 +- .../kotlin/net/corda/testing/CoreTestUtils.kt | 24 +++++-- .../net/corda/testing/LedgerDSLInterpreter.kt | 1 - .../net/corda/testing}/TestConstants.kt | 14 +--- .../testing/TransactionDSLInterpreter.kt | 1 - .../corda/testing/contracts}/VaultFiller.kt | 23 +++---- .../kotlin/net/corda/testing/driver/Driver.kt | 2 +- .../testing/driver/NetworkMapStartStrategy.kt | 2 +- .../corda/testing/node/MockNetworkMapCache.kt | 2 +- .../kotlin/net/corda/testing/node/MockNode.kt | 9 +-- .../net/corda/testing/node/MockServices.kt | 4 +- .../net/corda/testing/node/NodeBasedTest.kt | 2 +- tools/demobench/build.gradle | 2 +- .../corda/demobench/model/NodeConfigTest.kt | 2 +- .../demobench/model/NodeControllerTest.kt | 2 +- .../net/corda/explorer/ExplorerSimulation.kt | 15 ++--- .../net/corda/verifier/VerifierTests.kt | 4 +- .../corda/webserver/WebserverDriverTests.kt | 2 +- 109 files changed, 300 insertions(+), 306 deletions(-) rename {core/src/main/kotlin/net/corda/core/utilities => test-utils/src/main/kotlin/net/corda/testing}/TestConstants.kt (81%) rename {finance/src/main/kotlin/net/corda/contracts/testing => test-utils/src/main/kotlin/net/corda/testing/contracts}/VaultFiller.kt (92%) diff --git a/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt b/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt index a3b9708610..94436ea72b 100644 --- a/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt +++ b/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt @@ -21,10 +21,10 @@ import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.Vault import net.corda.core.serialization.OpaqueBytes import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.BOB -import net.corda.core.utilities.CHARLIE -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.ALICE +import net.corda.testing.BOB +import net.corda.testing.CHARLIE +import net.corda.testing.DUMMY_NOTARY import net.corda.flows.CashExitFlow import net.corda.flows.CashIssueFlow import net.corda.flows.CashPaymentFlow diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt index 72f3ef6316..944bc37b2e 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt @@ -7,7 +7,7 @@ import net.corda.core.messaging.* import net.corda.core.node.services.ServiceInfo import net.corda.core.random63BitValue import net.corda.core.serialization.OpaqueBytes -import net.corda.core.utilities.ALICE +import net.corda.testing.ALICE import net.corda.flows.CashException import net.corda.flows.CashIssueFlow import net.corda.flows.CashPaymentFlow diff --git a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt index e2cdf44958..eab25e0eb6 100644 --- a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt +++ b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt @@ -20,7 +20,6 @@ import net.corda.core.node.services.vault.SortAttribute import net.corda.core.seconds import net.corda.core.serialization.OpaqueBytes import net.corda.core.sizedInputStreamAndHash -import net.corda.core.utilities.DUMMY_NOTARY import net.corda.core.utilities.loggerFor import net.corda.flows.CashIssueFlow import net.corda.flows.CashPaymentFlow @@ -28,6 +27,7 @@ import net.corda.nodeapi.User import net.corda.smoketesting.NodeConfig import net.corda.smoketesting.NodeProcess import org.apache.commons.io.output.NullOutputStream +import org.bouncycastle.asn1.x500.X500Name import org.junit.After import org.junit.Before import org.junit.Test @@ -54,7 +54,7 @@ class StandaloneCordaRPClientTest { private lateinit var notaryNode: NodeInfo private val notaryConfig = NodeConfig( - party = DUMMY_NOTARY, + legalName = X500Name("CN=Notary Service,O=R3,OU=corda,L=Zurich,C=CH"), p2pPort = port.andIncrement, rpcPort = port.andIncrement, webPort = port.andIncrement, @@ -115,7 +115,7 @@ class StandaloneCordaRPClientTest { @Test fun `test network map`() { - assertEquals(DUMMY_NOTARY.name, notaryNode.legalIdentity.name) + assertEquals(notaryConfig.legalName, notaryNode.legalIdentity.name) } @Test diff --git a/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt b/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt index 802d275bd5..81274dfa52 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt @@ -3,8 +3,8 @@ package net.corda.core.contracts import net.corda.core.contracts.testing.DummyContract import net.corda.core.contracts.testing.DummyContractV2 import net.corda.core.crypto.SecureHash -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.ALICE +import net.corda.testing.DUMMY_NOTARY import org.junit.Test import kotlin.test.assertEquals import kotlin.test.assertTrue diff --git a/core/src/test/kotlin/net/corda/core/contracts/TransactionGraphSearchTests.kt b/core/src/test/kotlin/net/corda/core/contracts/TransactionGraphSearchTests.kt index aaa09b7ef5..f48e5b920b 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/TransactionGraphSearchTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/TransactionGraphSearchTests.kt @@ -5,8 +5,8 @@ import net.corda.core.contracts.testing.DummyState import net.corda.core.crypto.newSecureRandom import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.core.utilities.DUMMY_NOTARY_KEY +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY_KEY import net.corda.testing.MEGA_CORP_KEY import net.corda.testing.MEGA_CORP_PUBKEY import net.corda.testing.node.MockServices diff --git a/core/src/test/kotlin/net/corda/core/contracts/TransactionTests.kt b/core/src/test/kotlin/net/corda/core/contracts/TransactionTests.kt index c42ffb4a28..f4bbe7f3f4 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/TransactionTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/TransactionTests.kt @@ -11,7 +11,7 @@ import net.corda.core.serialization.SerializedBytes import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.* +import net.corda.testing.* import org.junit.Test import java.security.KeyPair import kotlin.test.assertEquals diff --git a/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt b/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt index 2452fc8466..6b69c32e13 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt @@ -9,9 +9,9 @@ import net.corda.core.identity.Party import net.corda.core.serialization.p2PKryo import net.corda.core.serialization.serialize import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.core.utilities.DUMMY_PUBKEY_1 -import net.corda.core.utilities.TEST_TX_TIME +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.DUMMY_PUBKEY_1 +import net.corda.testing.TEST_TX_TIME import net.corda.testing.* import org.junit.Test import java.security.PublicKey diff --git a/core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt index 852c14b4fb..87d61276de 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt @@ -6,7 +6,7 @@ import net.corda.core.getOrThrow import net.corda.core.identity.Party import net.corda.core.serialization.opaque import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.DUMMY_NOTARY_KEY +import net.corda.testing.DUMMY_NOTARY_KEY import net.corda.flows.ResolveTransactionsFlow import net.corda.node.utilities.transaction import net.corda.testing.MEGA_CORP diff --git a/core/src/test/kotlin/net/corda/core/identity/PartyTest.kt b/core/src/test/kotlin/net/corda/core/identity/PartyTest.kt index 37fe78f1ba..707e5b159b 100644 --- a/core/src/test/kotlin/net/corda/core/identity/PartyTest.kt +++ b/core/src/test/kotlin/net/corda/core/identity/PartyTest.kt @@ -1,7 +1,7 @@ package net.corda.core.identity import net.corda.core.crypto.entropyToKeyPair -import net.corda.core.utilities.ALICE +import net.corda.testing.ALICE import org.junit.Test import java.math.BigInteger import kotlin.test.assertEquals diff --git a/core/src/test/kotlin/net/corda/core/node/AttachmentClassLoaderTests.kt b/core/src/test/kotlin/net/corda/core/node/AttachmentClassLoaderTests.kt index 5a58def9fd..e2af5fe66b 100644 --- a/core/src/test/kotlin/net/corda/core/node/AttachmentClassLoaderTests.kt +++ b/core/src/test/kotlin/net/corda/core/node/AttachmentClassLoaderTests.kt @@ -10,7 +10,7 @@ import net.corda.core.identity.Party import net.corda.core.node.services.AttachmentStorage import net.corda.core.serialization.* import net.corda.core.transactions.TransactionBuilder -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.MEGA_CORP import net.corda.testing.node.MockAttachmentStorage import org.apache.commons.io.IOUtils diff --git a/core/src/test/kotlin/net/corda/core/node/VaultUpdateTests.kt b/core/src/test/kotlin/net/corda/core/node/VaultUpdateTests.kt index 9b15ca200e..a2f2e81716 100644 --- a/core/src/test/kotlin/net/corda/core/node/VaultUpdateTests.kt +++ b/core/src/test/kotlin/net/corda/core/node/VaultUpdateTests.kt @@ -4,7 +4,7 @@ import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.identity.AbstractParty import net.corda.core.node.services.Vault -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY import org.junit.Test import kotlin.test.assertEquals diff --git a/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt b/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt index 0852846808..daa27fe0cb 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt @@ -3,8 +3,8 @@ package net.corda.core.serialization import com.esotericsoftware.kryo.Kryo import com.google.common.primitives.Ints import net.corda.core.crypto.* -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.BOB +import net.corda.testing.ALICE +import net.corda.testing.BOB import net.corda.node.services.messaging.Ack import net.corda.node.services.persistence.NodeAttachmentService import net.corda.testing.BOB_PUBKEY diff --git a/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt b/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt index d006cdb134..4e34ebfb07 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt @@ -5,7 +5,6 @@ import net.corda.core.crypto.SecureHash import net.corda.core.identity.AbstractParty import net.corda.core.seconds import net.corda.core.transactions.TransactionBuilder -import net.corda.core.utilities.* import net.corda.testing.* import net.corda.testing.node.MockServices import org.junit.Before diff --git a/core/src/test/kotlin/net/corda/flows/TransactionKeyFlowTests.kt b/core/src/test/kotlin/net/corda/flows/TransactionKeyFlowTests.kt index ce76532392..81480b842b 100644 --- a/core/src/test/kotlin/net/corda/flows/TransactionKeyFlowTests.kt +++ b/core/src/test/kotlin/net/corda/flows/TransactionKeyFlowTests.kt @@ -3,9 +3,9 @@ package net.corda.flows import net.corda.core.getOrThrow import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.BOB -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.ALICE +import net.corda.testing.BOB +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.node.MockNetwork import org.junit.Before import org.junit.Test diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 3c9d316545..e95a2ebc56 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -17,6 +17,9 @@ UNRELEASED * ``ServiceHub.storageService`` has been removed. ``attachments`` and ``validatedTransactions`` are now direct members of ``ServiceHub``. +* Mock identity constants used in tests, such as ``ALICE``, ``BOB``, ``DUMMY_NOTARY``, have moved to ``net.corda.testing`` + in the ``test-utils`` module. + Milestone 13 ------------ diff --git a/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt index bd66479c80..51ca156e6d 100644 --- a/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt +++ b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt @@ -9,9 +9,9 @@ import net.corda.core.messaging.startFlow import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.Vault import net.corda.core.serialization.OpaqueBytes -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.BOB -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.ALICE +import net.corda.testing.BOB +import net.corda.testing.DUMMY_NOTARY import net.corda.flows.CashIssueFlow import net.corda.flows.CashPaymentFlow import net.corda.testing.driver.driver diff --git a/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java b/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java index 68e660d739..be0cd0e03e 100644 --- a/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java +++ b/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java @@ -36,7 +36,7 @@ import java.util.List; import java.util.Set; import static net.corda.core.contracts.ContractsDSL.requireThat; -import static net.corda.core.utilities.TestConstants.getDUMMY_PUBKEY_1; +import static net.corda.testing.TestConstants.getDUMMY_PUBKEY_1; // We group our two flows inside a singleton object to indicate that they work // together. diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt index dd870c27d0..0581e63abd 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt @@ -12,8 +12,8 @@ import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.OpaqueBytes import net.corda.core.serialization.SerializationCustomization import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.ALICE +import net.corda.testing.DUMMY_NOTARY import net.corda.flows.CashExitFlow import net.corda.flows.CashIssueFlow import net.corda.flows.CashPaymentFlow diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt index acb355c09c..bff2303dcf 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt @@ -18,7 +18,7 @@ import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.DUMMY_PUBKEY_1 +import net.corda.testing.DUMMY_PUBKEY_1 import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker.Step import net.corda.core.utilities.UntrustworthyData diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt index 5f865dd02d..178d4c9b9e 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt @@ -5,8 +5,8 @@ import net.corda.core.getOrThrow import net.corda.core.node.services.ServiceInfo import net.corda.core.serialization.OpaqueBytes import net.corda.core.toFuture -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.core.utilities.DUMMY_NOTARY_KEY +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY_KEY import net.corda.flows.CashIssueFlow import net.corda.node.services.network.NetworkMapService import net.corda.node.services.transactions.ValidatingNotaryService diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt index a997d06746..2e165793f4 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt @@ -11,8 +11,8 @@ import net.corda.core.node.services.queryBy import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.and import net.corda.core.toFuture -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.core.utilities.DUMMY_NOTARY_KEY +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY_KEY import net.corda.node.services.network.NetworkMapService import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.node.utilities.transaction diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/Cap.kt b/experimental/src/test/kotlin/net/corda/contracts/universal/Cap.kt index e73e8d96db..c4c8767d95 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/Cap.kt +++ b/experimental/src/test/kotlin/net/corda/contracts/universal/Cap.kt @@ -4,7 +4,7 @@ import net.corda.contracts.BusinessCalendar import net.corda.contracts.FixOf import net.corda.contracts.Frequency import net.corda.contracts.Tenor -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.transaction import org.junit.Ignore import org.junit.Test diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/Caplet.kt b/experimental/src/test/kotlin/net/corda/contracts/universal/Caplet.kt index 63e955647e..0e475216a3 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/Caplet.kt +++ b/experimental/src/test/kotlin/net/corda/contracts/universal/Caplet.kt @@ -2,7 +2,7 @@ package net.corda.contracts.universal import net.corda.contracts.FixOf import net.corda.contracts.Tenor -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.transaction import org.junit.Ignore import org.junit.Test diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/ContractDefinition.kt b/experimental/src/test/kotlin/net/corda/contracts/universal/ContractDefinition.kt index 057c867490..e01f989b8d 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/ContractDefinition.kt +++ b/experimental/src/test/kotlin/net/corda/contracts/universal/ContractDefinition.kt @@ -2,7 +2,7 @@ package net.corda.contracts.universal import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.Party -import net.corda.core.utilities.ALICE +import net.corda.testing.ALICE import net.corda.testing.MEGA_CORP import net.corda.testing.MINI_CORP import org.junit.Test diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/FXFwdTimeOption.kt b/experimental/src/test/kotlin/net/corda/contracts/universal/FXFwdTimeOption.kt index 73abc393cb..51c2ab4dd3 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/FXFwdTimeOption.kt +++ b/experimental/src/test/kotlin/net/corda/contracts/universal/FXFwdTimeOption.kt @@ -1,6 +1,6 @@ package net.corda.contracts.universal -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.transaction import org.junit.Ignore import org.junit.Test diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/FXSwap.kt b/experimental/src/test/kotlin/net/corda/contracts/universal/FXSwap.kt index 755c9c2b98..e3cba5fdd7 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/FXSwap.kt +++ b/experimental/src/test/kotlin/net/corda/contracts/universal/FXSwap.kt @@ -1,6 +1,6 @@ package net.corda.contracts.universal -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.transaction import org.junit.Ignore import org.junit.Test diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/IRS.kt b/experimental/src/test/kotlin/net/corda/contracts/universal/IRS.kt index 612dd47c25..abdc4bb445 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/IRS.kt +++ b/experimental/src/test/kotlin/net/corda/contracts/universal/IRS.kt @@ -3,7 +3,7 @@ package net.corda.contracts.universal import net.corda.contracts.FixOf import net.corda.contracts.Frequency import net.corda.contracts.Tenor -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.transaction import org.junit.Ignore import org.junit.Test diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/RollOutTests.kt b/experimental/src/test/kotlin/net/corda/contracts/universal/RollOutTests.kt index a51fda60f4..aeccd75e75 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/RollOutTests.kt +++ b/experimental/src/test/kotlin/net/corda/contracts/universal/RollOutTests.kt @@ -1,7 +1,7 @@ package net.corda.contracts.universal import net.corda.contracts.Frequency -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.transaction import org.junit.Test import java.time.Instant diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/Swaption.kt b/experimental/src/test/kotlin/net/corda/contracts/universal/Swaption.kt index 5e89376f77..e71bd6746c 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/Swaption.kt +++ b/experimental/src/test/kotlin/net/corda/contracts/universal/Swaption.kt @@ -2,7 +2,7 @@ package net.corda.contracts.universal import net.corda.contracts.Frequency import net.corda.contracts.Tenor -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.transaction import org.junit.Ignore import org.junit.Test diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/ZeroCouponBond.kt b/experimental/src/test/kotlin/net/corda/contracts/universal/ZeroCouponBond.kt index 09b4aef290..3f41d0c260 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/ZeroCouponBond.kt +++ b/experimental/src/test/kotlin/net/corda/contracts/universal/ZeroCouponBond.kt @@ -1,6 +1,6 @@ package net.corda.contracts.universal -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.transaction import org.junit.Test import java.time.Instant diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt b/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt index 2f5dc79aee..02bff607b4 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt @@ -19,8 +19,6 @@ import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.Emoji import net.corda.core.utilities.NonEmptySet -import net.corda.core.utilities.TEST_TX_TIME -import net.corda.core.utilities.nonEmptySetOf import org.bouncycastle.asn1.x500.X500Name import java.math.BigInteger import java.security.PublicKey @@ -727,8 +725,3 @@ infix fun Obligation.State.`issued by`(party: AbstractParty) = copy val DUMMY_OBLIGATION_ISSUER_KEY by lazy { entropyToKeyPair(BigInteger.valueOf(10)) } /** A dummy, randomly generated issuer party by the name of "Snake Oil Issuer" */ val DUMMY_OBLIGATION_ISSUER by lazy { Party(X500Name("CN=Snake Oil Issuer,O=R3,OU=corda,L=London,C=GB"), DUMMY_OBLIGATION_ISSUER_KEY.public) } - -val Issued.OBLIGATION_DEF: Obligation.Terms - get() = Obligation.Terms(nonEmptySetOf(Cash().legalContractReference), nonEmptySetOf(this), TEST_TX_TIME) -val Amount>.OBLIGATION: Obligation.State - get() = Obligation.State(Obligation.Lifecycle.NORMAL, DUMMY_OBLIGATION_ISSUER, token.OBLIGATION_DEF, quantity, NULL_PARTY) diff --git a/finance/src/test/java/net/corda/contracts/asset/CashTestsJava.java b/finance/src/test/java/net/corda/contracts/asset/CashTestsJava.java index 2c8b2d4595..608df6a980 100644 --- a/finance/src/test/java/net/corda/contracts/asset/CashTestsJava.java +++ b/finance/src/test/java/net/corda/contracts/asset/CashTestsJava.java @@ -8,8 +8,8 @@ import org.junit.Test; import static net.corda.core.contracts.ContractsDSL.DOLLARS; import static net.corda.core.contracts.ContractsDSL.issuedBy; -import static net.corda.core.utilities.TestConstants.getDUMMY_PUBKEY_1; -import static net.corda.core.utilities.TestConstants.getDUMMY_PUBKEY_2; +import static net.corda.testing.TestConstants.getDUMMY_PUBKEY_1; +import static net.corda.testing.TestConstants.getDUMMY_PUBKEY_2; import static net.corda.testing.CoreTestUtils.*; /** diff --git a/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt b/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt index 404de8f62d..35bf37e01e 100644 --- a/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt @@ -1,7 +1,7 @@ package net.corda.contracts import net.corda.contracts.asset.* -import net.corda.contracts.testing.fillWithSomeTestCash +import net.corda.testing.contracts.fillWithSomeTestCash import net.corda.core.contracts.* import net.corda.core.days import net.corda.core.identity.AnonymousParty @@ -10,7 +10,6 @@ import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultService import net.corda.core.seconds import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.* import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction import net.corda.testing.* diff --git a/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt b/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt index 27f355d1a7..8477a66428 100644 --- a/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt @@ -1,6 +1,6 @@ package net.corda.contracts.asset -import net.corda.contracts.testing.fillWithSomeTestCash +import net.corda.testing.contracts.fillWithSomeTestCash import net.corda.core.contracts.* import net.corda.core.contracts.testing.DummyState import net.corda.core.crypto.SecureHash diff --git a/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt b/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt index 8cb3d8f677..541b9b9997 100644 --- a/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt @@ -6,6 +6,7 @@ import net.corda.contracts.asset.Obligation.Lifecycle import net.corda.core.contracts.* import net.corda.core.contracts.testing.DummyState import net.corda.core.crypto.SecureHash +import net.corda.core.hours import net.corda.core.crypto.testing.NULL_PARTY import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty @@ -15,6 +16,7 @@ import net.corda.testing.* import net.corda.testing.node.MockServices import org.junit.Test import java.time.Duration +import java.time.Instant import java.time.temporal.ChronoUnit import java.util.* import kotlin.test.assertEquals @@ -23,14 +25,14 @@ import kotlin.test.assertNotEquals import kotlin.test.assertTrue class ObligationTests { - val defaultRef = OpaqueBytes(ByteArray(1, { 1 })) + val defaultRef = OpaqueBytes.of(1) val defaultIssuer = MEGA_CORP.ref(defaultRef) val oneMillionDollars = 1000000.DOLLARS `issued by` defaultIssuer val trustedCashContract = nonEmptySetOf(SecureHash.randomSHA256() as SecureHash) val megaIssuedDollars = nonEmptySetOf(Issued(defaultIssuer, USD)) val megaIssuedPounds = nonEmptySetOf(Issued(defaultIssuer, GBP)) - val fivePm = TEST_TX_TIME.truncatedTo(ChronoUnit.DAYS).plus(17, ChronoUnit.HOURS) - val sixPm = fivePm.plus(1, ChronoUnit.HOURS) + val fivePm: Instant = TEST_TX_TIME.truncatedTo(ChronoUnit.DAYS) + 17.hours + val sixPm: Instant = fivePm + 1.hours val megaCorpDollarSettlement = Obligation.Terms(trustedCashContract, megaIssuedDollars, fivePm) val megaCorpPoundSettlement = megaCorpDollarSettlement.copy(acceptableIssuedProducts = megaIssuedPounds) val inState = Obligation.State( @@ -871,4 +873,9 @@ class ObligationTests { val actual = sumAmountsDue(balanced) assertEquals(expected, actual) } + + val Issued.OBLIGATION_DEF: Obligation.Terms + get() = Obligation.Terms(nonEmptySetOf(Cash().legalContractReference), nonEmptySetOf(this), TEST_TX_TIME) + val Amount>.OBLIGATION: Obligation.State + get() = Obligation.State(Obligation.Lifecycle.NORMAL, DUMMY_OBLIGATION_ISSUER, token.OBLIGATION_DEF, quantity, NULL_PARTY) } diff --git a/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt b/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt index 7d1714933a..607da6fbfb 100644 --- a/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt +++ b/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt @@ -1,7 +1,7 @@ package net.corda.flows import com.google.common.util.concurrent.ListenableFuture -import net.corda.contracts.testing.calculateRandomlySizedAmounts +import net.corda.testing.contracts.calculateRandomlySizedAmounts import net.corda.core.contracts.Amount import net.corda.core.contracts.DOLLARS import net.corda.core.contracts.currency @@ -13,7 +13,7 @@ import net.corda.core.map import net.corda.core.serialization.OpaqueBytes import net.corda.core.toFuture import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY import net.corda.flows.IssuerFlow.IssuanceRequester import net.corda.testing.BOC import net.corda.testing.MEGA_CORP diff --git a/node-schemas/build.gradle b/node-schemas/build.gradle index b08f45d361..88a9102a8b 100644 --- a/node-schemas/build.gradle +++ b/node-schemas/build.gradle @@ -8,12 +8,14 @@ description 'Corda node database schemas' dependencies { compile project(':core') - testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" - testCompile "junit:junit:$junit_version" // Requery: SQL based query & persistence for Kotlin kapt "io.requery:requery-processor:$requery_version" + testCompile project(':test-utils') + testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" + testCompile "junit:junit:$junit_version" + // For H2 database support in persistence testCompile "com.h2database:h2:$h2_version" } diff --git a/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt b/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt index 5d552f6a9e..b597a8188d 100644 --- a/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt +++ b/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt @@ -21,11 +21,11 @@ import net.corda.core.schemas.requery.converters.VaultStateStatusConverter import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.transactions.LedgerTransaction -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.BOB -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.core.utilities.DUMMY_NOTARY_KEY import net.corda.node.services.vault.schemas.requery.* +import net.corda.testing.ALICE +import net.corda.testing.BOB +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY_KEY import org.h2.jdbcx.JdbcDataSource import org.junit.After import org.junit.Assert diff --git a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt index d2f8ebff8e..b5366a7ace 100644 --- a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt @@ -8,7 +8,7 @@ import net.corda.core.getOrThrow import net.corda.core.messaging.startFlow import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceType -import net.corda.core.utilities.ALICE +import net.corda.testing.ALICE import net.corda.testing.driver.driver import net.corda.node.internal.NodeStartup import net.corda.node.services.startFlowPermission diff --git a/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt b/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt index 1e933ddaf5..0d4f786dcf 100644 --- a/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt @@ -9,8 +9,8 @@ import net.corda.core.flows.StartableByRPC import net.corda.core.getOrThrow import net.corda.core.identity.Party import net.corda.core.messaging.startFlow -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.BOB +import net.corda.testing.ALICE +import net.corda.testing.BOB import net.corda.core.utilities.unwrap import net.corda.node.services.startFlowPermission import net.corda.nodeapi.User diff --git a/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt index f5ec2cbeae..219d730901 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt @@ -10,8 +10,8 @@ import net.corda.core.messaging.StateMachineUpdate import net.corda.core.messaging.startFlow import net.corda.core.node.NodeInfo import net.corda.core.serialization.OpaqueBytes -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.ALICE +import net.corda.testing.DUMMY_NOTARY import net.corda.flows.CashIssueFlow import net.corda.flows.CashPaymentFlow import net.corda.testing.driver.NodeHandle diff --git a/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt index 6fe61fb40d..1075e83ad2 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt @@ -8,7 +8,7 @@ import net.corda.core.contracts.testing.DummyContract import net.corda.core.getOrThrow import net.corda.core.identity.Party import net.corda.core.map -import net.corda.core.utilities.DUMMY_BANK_A +import net.corda.testing.DUMMY_BANK_A import net.corda.flows.NotaryError import net.corda.flows.NotaryException import net.corda.flows.NotaryFlow diff --git a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowVersioningTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowVersioningTest.kt index a2cedb2ee0..459310ecf7 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowVersioningTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowVersioningTest.kt @@ -6,8 +6,8 @@ import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow import net.corda.core.getOrThrow import net.corda.core.identity.Party -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.BOB +import net.corda.testing.ALICE +import net.corda.testing.BOB import net.corda.core.utilities.unwrap import net.corda.testing.node.NodeBasedTest import org.assertj.core.api.Assertions.assertThat diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt index 7086d1a3db..f549064a6f 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt @@ -12,8 +12,8 @@ import net.corda.core.getOrThrow import net.corda.core.identity.Party import net.corda.core.messaging.CordaRPCOps import net.corda.core.random63BitValue -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.BOB +import net.corda.testing.ALICE +import net.corda.testing.BOB import net.corda.core.utilities.unwrap import net.corda.node.internal.Node import net.corda.nodeapi.ArtemisMessagingComponent.Companion.INTERNAL_PREFIX diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt index 9ba5bac762..af7c8635bd 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt @@ -10,14 +10,12 @@ import net.corda.core.node.services.ServiceInfo import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize -import net.corda.core.utilities.* import net.corda.node.internal.Node import net.corda.node.services.messaging.* import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.node.services.transactions.SimpleNotaryService import net.corda.node.utilities.ServiceIdentityGenerator -import net.corda.testing.freeLocalHostAndPort -import net.corda.testing.getTestX509Name +import net.corda.testing.* import net.corda.testing.node.NodeBasedTest import org.assertj.core.api.Assertions.assertThat import org.bouncycastle.asn1.x500.X500Name diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt index 49dfc8a119..a5789b3b7b 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt @@ -8,7 +8,6 @@ import net.corda.core.getOrThrow import net.corda.core.node.NodeInfo import net.corda.core.random63BitValue import net.corda.core.seconds -import net.corda.core.utilities.* import net.corda.node.internal.NetworkMapInfo import net.corda.node.services.config.configureWithDevSSLCertificate import net.corda.node.services.messaging.sendRequest @@ -16,11 +15,9 @@ import net.corda.node.services.network.NetworkMapService import net.corda.node.services.network.NetworkMapService.RegistrationRequest import net.corda.node.services.network.NodeRegistration import net.corda.node.utilities.AddOrRemove -import net.corda.testing.MOCK_HOST_AND_PORT -import net.corda.testing.MOCK_VERSION_INFO +import net.corda.testing.* import net.corda.testing.node.NodeBasedTest import net.corda.testing.node.SimpleNode -import net.corda.testing.testNodeConfiguration import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.assertj.core.api.Assertions.assertThatThrownBy import org.bouncycastle.asn1.x500.X500Name diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index a6202cedd9..a01f3b4045 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -24,9 +24,7 @@ import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.deserialize import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.DUMMY_CA import net.corda.core.utilities.debug -import net.corda.core.utilities.getTestPartyAndCertificate import net.corda.flows.* import net.corda.node.services.* import net.corda.node.services.api.* @@ -71,6 +69,7 @@ import rx.Observable import java.io.IOException import java.lang.reflect.InvocationTargetException import java.lang.reflect.Modifier.* +import java.math.BigInteger import java.net.JarURLConnection import java.net.URI import java.nio.file.Path @@ -735,9 +734,13 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, if (myIdentity.owningKey !is CompositeKey) { // TODO: Support case where owningKey is a composite key. keyStore.save(serviceName, privateKeyAlias, keyPair) } - val partyAndCertificate = getTestPartyAndCertificate(myIdentity) + val dummyCaKey = entropyToKeyPair(BigInteger.valueOf(111)) + val dummyCa = CertificateAndKeyPair( + X509Utilities.createSelfSignedCACertificate(X500Name("CN=Dummy CA,OU=Corda,O=R3 Ltd,L=London,C=GB"), dummyCaKey), + dummyCaKey) + val partyAndCertificate = getTestPartyAndCertificate(myIdentity, dummyCa) // Sanity check the certificate and path - val validatorParameters = PKIXParameters(setOf(TrustAnchor(DUMMY_CA.certificate.cert, null))) + val validatorParameters = PKIXParameters(setOf(TrustAnchor(dummyCa.certificate.cert, null))) val validator = CertPathValidator.getInstance("PKIX") validatorParameters.isRevocationEnabled = false validator.validate(partyAndCertificate.certPath, validatorParameters) as PKIXCertPathValidatorResult @@ -758,6 +761,13 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, return identityCertPathAndKey } + private fun getTestPartyAndCertificate(party: Party, trustRoot: CertificateAndKeyPair): 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) + } + protected open fun generateKeyPair() = cryptoGenerateKeyPair() private fun createAttachmentStorage(): NodeAttachmentService { diff --git a/node/src/smoke-test/kotlin/net/corda/node/CordappScanningNodeProcessTest.kt b/node/src/smoke-test/kotlin/net/corda/node/CordappScanningNodeProcessTest.kt index 1aaeb038ef..87b8faad10 100644 --- a/node/src/smoke-test/kotlin/net/corda/node/CordappScanningNodeProcessTest.kt +++ b/node/src/smoke-test/kotlin/net/corda/node/CordappScanningNodeProcessTest.kt @@ -3,11 +3,11 @@ package net.corda.node import net.corda.core.copyToDirectory import net.corda.core.createDirectories import net.corda.core.div -import net.corda.core.utilities.ALICE import net.corda.nodeapi.User import net.corda.smoketesting.NodeConfig import net.corda.smoketesting.NodeProcess import org.assertj.core.api.Assertions.assertThat +import org.bouncycastle.asn1.x500.X500Name import org.junit.Test import java.nio.file.Paths import java.util.concurrent.atomic.AtomicInteger @@ -21,7 +21,7 @@ class CordappScanningNodeProcessTest { private val factory = NodeProcess.Factory() private val aliceConfig = NodeConfig( - party = ALICE, + legalName = X500Name("CN=Alice Corp,O=Alice Corp,L=Madrid,C=ES"), p2pPort = port.andIncrement, rpcPort = port.andIncrement, webPort = port.andIncrement, diff --git a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java index 46f47c5773..3ec565aaa8 100644 --- a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java +++ b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java @@ -25,6 +25,8 @@ import net.corda.core.transactions.WireTransaction; import net.corda.node.services.database.HibernateConfiguration; import net.corda.node.services.schema.NodeSchemaService; import net.corda.schemas.CashSchemaV1; +import net.corda.testing.TestConstants; +import net.corda.testing.contracts.VaultFiller; import net.corda.testing.node.MockServices; import org.jetbrains.annotations.NotNull; import org.jetbrains.exposed.sql.Database; @@ -43,11 +45,10 @@ import java.util.stream.StreamSupport; import static net.corda.contracts.asset.CashKt.getDUMMY_CASH_ISSUER; import static net.corda.contracts.asset.CashKt.getDUMMY_CASH_ISSUER_KEY; -import static net.corda.contracts.testing.VaultFiller.*; +import static net.corda.core.contracts.ContractsDSL.USD; import static net.corda.core.node.services.vault.QueryCriteriaKt.and; import static net.corda.core.node.services.vault.QueryCriteriaKt.or; import static net.corda.core.node.services.vault.QueryCriteriaUtilsKt.getMAX_PAGE_SIZE; -import static net.corda.core.utilities.TestConstants.getDUMMY_NOTARY; import static net.corda.node.utilities.DatabaseSupportKt.configureDatabase; import static net.corda.node.utilities.DatabaseSupportKt.transaction; import static net.corda.testing.CoreTestUtils.getMEGA_CORP; @@ -70,7 +71,7 @@ public class VaultQueryJavaTests { dataSource = dataSourceAndDatabase.getFirst(); database = dataSourceAndDatabase.getSecond(); - Set customSchemas = new HashSet<>(Arrays.asList(DummyLinearStateSchemaV1.INSTANCE)); + Set customSchemas = new HashSet<>(Collections.singletonList(DummyLinearStateSchemaV1.INSTANCE)); HibernateConfiguration hibernateConfig = new HibernateConfiguration(new NodeSchemaService(customSchemas)); transaction(database, statement -> { services = new MockServices(getMEGA_CORP_KEY()) { @@ -119,7 +120,7 @@ public class VaultQueryJavaTests { public void unconsumedLinearStates() throws VaultQueryException { transaction(database, tx -> { - fillWithSomeTestLinearStates(services, 3); + VaultFiller.fillWithSomeTestLinearStates(services, 3); // DOCSTART VaultJavaQueryExample0 Vault.Page results = vaultQuerySvc.queryBy(LinearState.class); @@ -137,9 +138,9 @@ public class VaultQueryJavaTests { Amount amount = new Amount<>(100, Currency.getInstance("USD")); - fillWithSomeTestCash(services, + VaultFiller.fillWithSomeTestCash(services, new Amount<>(100, Currency.getInstance("USD")), - getDUMMY_NOTARY(), + TestConstants.getDUMMY_NOTARY(), 3, 3, new Random(), @@ -148,7 +149,7 @@ public class VaultQueryJavaTests { getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY() ); - consumeCash(services, amount); + VaultFiller.consumeCash(services, amount); // DOCSTART VaultJavaQueryExample1 VaultQueryCriteria criteria = new VaultQueryCriteria(Vault.StateStatus.CONSUMED); @@ -165,16 +166,16 @@ public class VaultQueryJavaTests { public void consumedDealStatesPagedSorted() throws VaultQueryException { transaction(database, tx -> { - Vault states = fillWithSomeTestLinearStates(services, 10, null); + Vault states = VaultFiller.fillWithSomeTestLinearStates(services, 10, null); StateAndRef linearState = states.getStates().iterator().next(); UniqueIdentifier uid = linearState.component1().getData().getLinearId(); List dealIds = Arrays.asList("123", "456", "789"); - Vault dealStates = fillWithSomeTestDeals(services, dealIds); + Vault dealStates = VaultFiller.fillWithSomeTestDeals(services, dealIds); // consume states - consumeDeals(services, (List>) dealStates.getStates()); - consumeLinearStates(services, Arrays.asList(linearState)); + VaultFiller.consumeDeals(services, (List>) dealStates.getStates()); + VaultFiller.consumeLinearStates(services, Collections.singletonList(linearState)); // DOCSTART VaultJavaQueryExample2 Vault.StateStatus status = Vault.StateStatus.CONSUMED; @@ -183,7 +184,7 @@ public class VaultQueryJavaTests { QueryCriteria vaultCriteria = new VaultQueryCriteria(status, contractStateTypes); - List linearIds = Arrays.asList(uid); + List linearIds = Collections.singletonList(uid); QueryCriteria linearCriteriaAll = new LinearStateQueryCriteria(null, linearIds); QueryCriteria dealCriteriaAll = new LinearStateQueryCriteria(null, null, dealIds); @@ -211,10 +212,10 @@ public class VaultQueryJavaTests { Amount dollars10 = new Amount<>(10, Currency.getInstance("USD")); Amount dollars1 = new Amount<>(1, Currency.getInstance("USD")); - fillWithSomeTestCash(services, pounds, getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); - fillWithSomeTestCash(services, dollars100, getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); - fillWithSomeTestCash(services, dollars10, getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); - fillWithSomeTestCash(services, dollars1, getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); + VaultFiller.fillWithSomeTestCash(services, pounds, TestConstants.getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); + VaultFiller.fillWithSomeTestCash(services, dollars100, TestConstants.getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); + VaultFiller.fillWithSomeTestCash(services, dollars10, TestConstants.getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); + VaultFiller.fillWithSomeTestCash(services, dollars1, TestConstants.getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); try { // DOCSTART VaultJavaQueryExample3 @@ -249,9 +250,9 @@ public class VaultQueryJavaTests { @Test public void trackCashStates() { transaction(database, tx -> { - fillWithSomeTestCash(services, + VaultFiller.fillWithSomeTestCash(services, new Amount<>(100, Currency.getInstance("USD")), - getDUMMY_NOTARY(), + TestConstants.getDUMMY_NOTARY(), 3, 3, new Random(), @@ -281,19 +282,19 @@ public class VaultQueryJavaTests { public void trackDealStatesPagedSorted() { transaction(database, tx -> { - Vault states = fillWithSomeTestLinearStates(services, 10, null); + Vault states = VaultFiller.fillWithSomeTestLinearStates(services, 10, null); UniqueIdentifier uid = states.getStates().iterator().next().component1().getData().getLinearId(); List dealIds = Arrays.asList("123", "456", "789"); - fillWithSomeTestDeals(services, dealIds); + VaultFiller.fillWithSomeTestDeals(services, dealIds); // DOCSTART VaultJavaQueryExample5 @SuppressWarnings("unchecked") Set> contractStateTypes = new HashSet(Arrays.asList(DealState.class, LinearState.class)); QueryCriteria vaultCriteria = new VaultQueryCriteria(Vault.StateStatus.UNCONSUMED, contractStateTypes); - List linearIds = Arrays.asList(uid); - List dealParty = Arrays.asList(getMEGA_CORP()); + List linearIds = Collections.singletonList(uid); + List dealParty = Collections.singletonList(getMEGA_CORP()); QueryCriteria dealCriteria = new LinearStateQueryCriteria(dealParty, null, dealIds); QueryCriteria linearCriteria = new LinearStateQueryCriteria(dealParty, linearIds, null); QueryCriteria dealOrLinearIdCriteria = or(dealCriteria, linearCriteria); @@ -304,8 +305,8 @@ public class VaultQueryJavaTests { Sort sorting = new Sort(ImmutableSet.of(sortByUid)); DataFeed, Vault.Update> results = vaultQuerySvc.trackBy(ContractState.class, compositeCriteria, pageSpec, sorting); - Vault.Page snapshot = results.getCurrent(); - Observable updates = results.getFuture(); + Vault.Page snapshot = results.getSnapshot(); + Observable updates = results.getUpdates(); // DOCEND VaultJavaQueryExample5 assertThat(snapshot.getStates()).hasSize(13); @@ -321,10 +322,10 @@ public class VaultQueryJavaTests { @Test public void consumedStatesDeprecated() { transaction(database, tx -> { - Amount amount = new Amount<>(100, Currency.getInstance("USD")); - fillWithSomeTestCash(services, - new Amount<>(100, Currency.getInstance("USD")), - getDUMMY_NOTARY(), + Amount amount = new Amount<>(100, USD); + VaultFiller.fillWithSomeTestCash(services, + new Amount<>(100, USD), + TestConstants.getDUMMY_NOTARY(), 3, 3, new Random(), @@ -333,7 +334,7 @@ public class VaultQueryJavaTests { getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY() ); - consumeCash(services, amount); + VaultFiller.consumeCash(services, amount); // DOCSTART VaultDeprecatedJavaQueryExample1 @SuppressWarnings("unchecked") @@ -354,10 +355,10 @@ public class VaultQueryJavaTests { public void consumedStatesForLinearIdDeprecated() { transaction(database, tx -> { - Vault linearStates = fillWithSomeTestLinearStates(services, 4,null); - UniqueIdentifier trackUid = linearStates.getStates().iterator().next().component1().getData().getLinearId(); + Vault linearStates = VaultFiller.fillWithSomeTestLinearStates(services, 4,null); + linearStates.getStates().iterator().next().component1().getData().getLinearId(); - consumeLinearStates(services, (List>) linearStates.getStates()); + VaultFiller.consumeLinearStates(services, (List>) linearStates.getStates()); // DOCSTART VaultDeprecatedJavaQueryExample0 @SuppressWarnings("unchecked") diff --git a/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt b/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt index 210bc46e2e..4a2c2051de 100644 --- a/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt +++ b/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt @@ -11,7 +11,7 @@ import net.corda.core.flows.StateMachineRunId import net.corda.core.identity.Party import net.corda.core.node.ServiceHub import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.DUMMY_CA +import net.corda.testing.DUMMY_CA import net.corda.core.utilities.UntrustworthyData import net.corda.jackson.JacksonSupport import net.corda.node.services.identity.InMemoryIdentityService diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index 79e1dae83c..9425372851 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -3,7 +3,7 @@ package net.corda.node.messaging import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.CommercialPaper import net.corda.contracts.asset.* -import net.corda.contracts.testing.fillWithSomeTestCash +import net.corda.testing.contracts.fillWithSomeTestCash import net.corda.core.* import net.corda.core.contracts.* import net.corda.core.crypto.DigitalSignature diff --git a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt index 346f124aaa..b7fae46b64 100644 --- a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt @@ -8,13 +8,13 @@ import net.corda.core.identity.Party import net.corda.core.node.services.ServiceInfo import net.corda.core.seconds import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.core.utilities.getTestPartyAndCertificate +import net.corda.testing.DUMMY_NOTARY import net.corda.flows.NotaryChangeFlow import net.corda.flows.StateReplacementException import net.corda.node.internal.AbstractNode import net.corda.node.services.network.NetworkMapService import net.corda.node.services.transactions.SimpleNotaryService +import net.corda.testing.getTestPartyAndCertificate import net.corda.testing.node.MockNetwork import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.bouncycastle.asn1.x500.X500Name diff --git a/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt index 01cfc1a8f0..3b8988ee0a 100644 --- a/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt @@ -2,7 +2,7 @@ package net.corda.node.services.config import com.google.common.net.HostAndPort import net.corda.core.crypto.commonName -import net.corda.core.utilities.ALICE +import net.corda.testing.ALICE import net.corda.nodeapi.User import net.corda.testing.node.makeTestDataSourceProperties import org.assertj.core.api.Assertions.assertThatThrownBy diff --git a/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt index ff5bfa324c..0514141451 100644 --- a/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt @@ -2,10 +2,10 @@ package net.corda.node.services.database import net.corda.contracts.asset.Cash import net.corda.contracts.asset.DummyFungibleContract -import net.corda.contracts.testing.consumeCash -import net.corda.contracts.testing.fillWithSomeTestCash -import net.corda.contracts.testing.fillWithSomeTestDeals -import net.corda.contracts.testing.fillWithSomeTestLinearStates +import net.corda.testing.contracts.consumeCash +import net.corda.testing.contracts.fillWithSomeTestCash +import net.corda.testing.contracts.fillWithSomeTestDeals +import net.corda.testing.contracts.fillWithSomeTestLinearStates import net.corda.core.contracts.* import net.corda.core.crypto.toBase58String import net.corda.core.node.services.Vault @@ -16,10 +16,10 @@ import net.corda.core.schemas.testing.DummyLinearStateSchemaV2 import net.corda.core.serialization.deserialize import net.corda.core.serialization.storageKryo import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.BOB -import net.corda.core.utilities.BOB_KEY -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.ALICE +import net.corda.testing.BOB +import net.corda.testing.BOB_KEY +import net.corda.testing.DUMMY_NOTARY import net.corda.node.services.schema.HibernateObserver import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.vault.NodeVaultService diff --git a/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt index 1ad35060e0..b0f5f06787 100644 --- a/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt @@ -16,8 +16,8 @@ import net.corda.core.serialization.serialize import net.corda.core.serialization.storageKryo import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.core.utilities.DUMMY_PUBKEY_1 +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.DUMMY_PUBKEY_1 import net.corda.node.services.persistence.DBTransactionStorage import net.corda.node.services.vault.schemas.requery.Models import net.corda.node.services.vault.schemas.requery.VaultCashBalancesEntity diff --git a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt index a17299eea2..c0e87230e0 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt @@ -9,9 +9,9 @@ import net.corda.core.identity.AbstractParty import net.corda.core.node.ServiceHub import net.corda.core.node.services.VaultService import net.corda.core.serialization.SingletonSerializeAsToken -import net.corda.core.utilities.ALICE_KEY -import net.corda.core.utilities.DUMMY_CA -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.ALICE_KEY +import net.corda.testing.DUMMY_CA +import net.corda.testing.DUMMY_NOTARY import net.corda.node.services.MockServiceHubInternal import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.persistence.DBCheckpointStorage diff --git a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt index e271045133..37b26f174f 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt @@ -12,7 +12,7 @@ import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.linearHeadsOfType -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY import net.corda.flows.FinalityFlow import net.corda.node.services.network.NetworkMapService import net.corda.node.services.statemachine.StateMachineManager diff --git a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt index 25b93c07e3..de4aaf1c0f 100644 --- a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt @@ -8,7 +8,7 @@ import com.google.common.util.concurrent.SettableFuture import net.corda.core.crypto.generateKeyPair import net.corda.core.messaging.RPCOps import net.corda.core.node.services.DEFAULT_SESSION_ID -import net.corda.core.utilities.ALICE +import net.corda.testing.ALICE import net.corda.core.utilities.LogHelper import net.corda.node.services.RPCUserService import net.corda.node.services.RPCUserServiceImpl diff --git a/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt index 300f2e95f7..9c6df604e7 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt @@ -7,10 +7,10 @@ import net.corda.core.node.NodeInfo import net.corda.core.node.services.DEFAULT_SESSION_ID import net.corda.core.node.services.ServiceInfo import net.corda.core.serialization.deserialize -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.BOB -import net.corda.core.utilities.CHARLIE -import net.corda.core.utilities.DUMMY_MAP +import net.corda.testing.ALICE +import net.corda.testing.BOB +import net.corda.testing.CHARLIE +import net.corda.testing.DUMMY_MAP import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.send import net.corda.node.services.messaging.sendRequest diff --git a/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt index ceaa7b36d9..053d85bfaa 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt @@ -5,11 +5,9 @@ import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.services.IdentityService -import net.corda.core.utilities.* import net.corda.flows.AnonymisedIdentity import net.corda.node.services.identity.InMemoryIdentityService -import net.corda.testing.ALICE_PUBKEY -import net.corda.testing.BOB_PUBKEY +import net.corda.testing.* import org.bouncycastle.asn1.x500.X500Name import org.junit.Test import java.security.cert.CertificateFactory @@ -28,13 +26,13 @@ class InMemoryIdentityServiceTests { assertNull(service.getAllIdentities().firstOrNull()) service.registerIdentity(ALICE_IDENTITY) - var expected = setOf(ALICE) + var expected = setOf(ALICE) var actual = service.getAllIdentities().map { it.party }.toHashSet() assertEquals(expected, actual) // Add a second party and check we get both back service.registerIdentity(BOB_IDENTITY) - expected = setOf(ALICE, BOB) + expected = setOf(ALICE, BOB) actual = service.getAllIdentities().map { it.party }.toHashSet() assertEquals(expected, actual) } diff --git a/node/src/test/kotlin/net/corda/node/services/network/InMemoryNetworkMapCacheTest.kt b/node/src/test/kotlin/net/corda/node/services/network/InMemoryNetworkMapCacheTest.kt index 05bfe43260..948228acb8 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/InMemoryNetworkMapCacheTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/InMemoryNetworkMapCacheTest.kt @@ -3,8 +3,8 @@ package net.corda.node.services.network import net.corda.core.getOrThrow import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.ServiceInfo -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.BOB +import net.corda.testing.ALICE +import net.corda.testing.BOB import net.corda.node.utilities.transaction import net.corda.testing.node.MockNetwork import org.junit.After diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt index 232db57467..fe4acd24b6 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt @@ -8,7 +8,7 @@ import net.corda.core.crypto.testing.NullPublicKey import net.corda.core.toFuture import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY import net.corda.core.utilities.LogHelper import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.node.utilities.configureDatabase diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DataVendingServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DataVendingServiceTests.kt index d5266caa8f..8f6181b639 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DataVendingServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DataVendingServiceTests.kt @@ -12,7 +12,7 @@ import net.corda.core.flows.InitiatingFlow import net.corda.core.identity.Party import net.corda.core.node.services.unconsumedStates import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY import net.corda.flows.BroadcastTransactionFlow.NotifyTxRequest import net.corda.node.services.NotifyTransactionHandler import net.corda.node.utilities.transaction diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt index 8259ab35c8..4e50c6e75e 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt @@ -10,7 +10,7 @@ import net.corda.core.getOrThrow import net.corda.core.node.services.ServiceInfo import net.corda.core.seconds import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY import net.corda.flows.NotaryError import net.corda.flows.NotaryException import net.corda.flows.NotaryFlow diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt index 267632521a..9b735f825a 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt @@ -10,7 +10,7 @@ import net.corda.core.crypto.DigitalSignature import net.corda.core.getOrThrow import net.corda.core.node.services.ServiceInfo import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY import net.corda.flows.NotaryError import net.corda.flows.NotaryException import net.corda.flows.NotaryFlow diff --git a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt index 1f9297c522..23feae2385 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt @@ -2,7 +2,7 @@ package net.corda.node.services.vault import net.corda.contracts.asset.Cash import net.corda.contracts.asset.DUMMY_CASH_ISSUER -import net.corda.contracts.testing.fillWithSomeTestCash +import net.corda.testing.contracts.fillWithSomeTestCash import net.corda.core.contracts.* import net.corda.core.identity.AnonymousParty import net.corda.core.node.services.StatesNotAvailableException @@ -10,7 +10,7 @@ import net.corda.core.node.services.VaultService import net.corda.core.node.services.unconsumedStates import net.corda.core.serialization.OpaqueBytes import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY import net.corda.core.utilities.LogHelper import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index 914a550e79..88c2467e35 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -6,7 +6,6 @@ import net.corda.contracts.DealState import net.corda.contracts.DummyDealContract import net.corda.contracts.asset.Cash import net.corda.contracts.asset.DUMMY_CASH_ISSUER -import net.corda.contracts.testing.* import net.corda.core.contracts.* import net.corda.core.contracts.testing.DummyLinearContract import net.corda.core.crypto.entropyToKeyPair @@ -19,9 +18,9 @@ import net.corda.core.schemas.testing.DummyLinearStateSchemaV1 import net.corda.core.seconds import net.corda.core.serialization.OpaqueBytes import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.core.utilities.DUMMY_NOTARY_KEY -import net.corda.core.utilities.TEST_TX_TIME +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY_KEY +import net.corda.testing.TEST_TX_TIME import net.corda.node.services.database.HibernateConfiguration import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.vault.schemas.jpa.VaultSchemaV1 @@ -32,6 +31,7 @@ import net.corda.schemas.CashSchemaV1.PersistentCashState import net.corda.schemas.CommercialPaperSchemaV1 import net.corda.schemas.SampleCashSchemaV3 import net.corda.testing.* +import net.corda.testing.contracts.* import net.corda.testing.node.MockServices import net.corda.testing.node.makeTestDataSourceProperties import org.assertj.core.api.Assertions diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt index 5e08269b5b..1c3de0094f 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt @@ -3,9 +3,9 @@ package net.corda.node.services.vault import net.corda.contracts.DummyDealContract import net.corda.contracts.asset.Cash import net.corda.contracts.asset.DUMMY_CASH_ISSUER -import net.corda.contracts.testing.fillWithSomeTestCash -import net.corda.contracts.testing.fillWithSomeTestDeals -import net.corda.contracts.testing.fillWithSomeTestLinearStates +import net.corda.testing.contracts.fillWithSomeTestCash +import net.corda.testing.contracts.fillWithSomeTestDeals +import net.corda.testing.contracts.fillWithSomeTestLinearStates import net.corda.core.contracts.* import net.corda.core.contracts.testing.DummyLinearContract import net.corda.core.identity.AnonymousParty @@ -13,9 +13,9 @@ import net.corda.core.node.services.VaultService import net.corda.core.node.services.consumedStates import net.corda.core.node.services.unconsumedStates import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.BOB -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.core.utilities.DUMMY_NOTARY_KEY +import net.corda.testing.BOB +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY_KEY import net.corda.core.utilities.LogHelper import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction diff --git a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt index 6e2ea2f3d3..201fb206f4 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt @@ -6,7 +6,7 @@ import com.nhaarman.mockito_kotlin.mock import net.corda.core.crypto.* import net.corda.core.exists import net.corda.core.toTypedArray -import net.corda.core.utilities.ALICE +import net.corda.testing.ALICE import net.corda.testing.getTestX509Name import net.corda.testing.testNodeConfiguration import org.bouncycastle.cert.X509CertificateHolder diff --git a/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt b/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt index 61f57c93b0..a7e1f16027 100644 --- a/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt +++ b/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt @@ -3,9 +3,9 @@ package net.corda.attachmentdemo import com.google.common.util.concurrent.Futures import net.corda.core.getOrThrow import net.corda.core.node.services.ServiceInfo -import net.corda.core.utilities.DUMMY_BANK_A -import net.corda.core.utilities.DUMMY_BANK_B -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_BANK_A +import net.corda.testing.DUMMY_BANK_B +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.driver.driver import net.corda.node.services.startFlowPermission import net.corda.node.services.transactions.SimpleNotaryService diff --git a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt index 82a7ed46b2..f66506808e 100644 --- a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt +++ b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt @@ -20,6 +20,8 @@ import net.corda.core.sizedInputStreamAndHash import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.* import net.corda.flows.FinalityFlow +import net.corda.testing.DUMMY_BANK_B +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.driver.poll import java.io.InputStream import java.net.HttpURLConnection diff --git a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/Main.kt b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/Main.kt index bfd97e359a..4528c419c0 100644 --- a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/Main.kt +++ b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/Main.kt @@ -2,9 +2,9 @@ package net.corda.attachmentdemo import net.corda.core.div import net.corda.core.node.services.ServiceInfo -import net.corda.core.utilities.DUMMY_BANK_A -import net.corda.core.utilities.DUMMY_BANK_B -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_BANK_A +import net.corda.testing.DUMMY_BANK_B +import net.corda.testing.DUMMY_NOTARY import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User import net.corda.testing.driver.driver diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt index d905bc06b5..3f9622d293 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt @@ -4,11 +4,10 @@ import com.google.common.net.HostAndPort import joptsimple.OptionParser import net.corda.bank.api.BankOfCordaClientApi import net.corda.bank.api.BankOfCordaWebApi.IssueRequestParams -import net.corda.core.crypto.X509Utilities import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceType import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY import net.corda.flows.CashExitFlow import net.corda.flows.CashPaymentFlow import net.corda.flows.IssuerFlow diff --git a/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt b/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt index c68008daf8..ce8cfee139 100644 --- a/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt +++ b/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt @@ -6,9 +6,9 @@ import net.corda.client.rpc.CordaRPCClient import net.corda.core.getOrThrow import net.corda.core.node.services.ServiceInfo import net.corda.core.toFuture -import net.corda.core.utilities.DUMMY_BANK_A -import net.corda.core.utilities.DUMMY_BANK_B -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_BANK_A +import net.corda.testing.DUMMY_BANK_B +import net.corda.testing.DUMMY_NOTARY import net.corda.irs.api.NodeInterestRates import net.corda.irs.contract.InterestRateSwap import net.corda.irs.utilities.uploadFile diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/Main.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/Main.kt index c229067712..321180d97d 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/Main.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/Main.kt @@ -3,9 +3,9 @@ package net.corda.irs import com.google.common.util.concurrent.Futures import net.corda.core.getOrThrow import net.corda.core.node.services.ServiceInfo -import net.corda.core.utilities.DUMMY_BANK_A -import net.corda.core.utilities.DUMMY_BANK_B -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_BANK_A +import net.corda.testing.DUMMY_BANK_B +import net.corda.testing.DUMMY_NOTARY import net.corda.irs.api.NodeInterestRates import net.corda.node.services.transactions.SimpleNotaryService import net.corda.testing.driver.driver diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt index 9d23175c3f..e040a73a71 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt @@ -14,8 +14,8 @@ import net.corda.core.getOrThrow import net.corda.core.identity.Party import net.corda.core.node.services.ServiceInfo import net.corda.core.transactions.TransactionBuilder -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.ALICE +import net.corda.testing.DUMMY_NOTARY import net.corda.core.utilities.LogHelper import net.corda.core.utilities.ProgressTracker import net.corda.irs.flows.RatesFixFlow diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/contract/IRSTests.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/contract/IRSTests.kt index e1c68fab13..fc01eddcaa 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/contract/IRSTests.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/contract/IRSTests.kt @@ -4,9 +4,9 @@ import net.corda.contracts.* import net.corda.core.contracts.* import net.corda.core.seconds import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.core.utilities.DUMMY_NOTARY_KEY -import net.corda.core.utilities.TEST_TX_TIME +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY_KEY +import net.corda.testing.TEST_TX_TIME import net.corda.testing.* import net.corda.testing.node.MockServices import org.junit.Test diff --git a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt index 468c269b02..59b0f2497d 100644 --- a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt +++ b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt @@ -17,7 +17,7 @@ import net.corda.core.flows.InitiatingFlow import net.corda.core.identity.Party import net.corda.core.node.services.linearHeadsOfType import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.DUMMY_CA +import net.corda.testing.DUMMY_CA import net.corda.flows.TwoPartyDealFlow.Acceptor import net.corda.flows.TwoPartyDealFlow.AutoOffer import net.corda.flows.TwoPartyDealFlow.Instigator diff --git a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt index baae371d8c..1548e83ef6 100644 --- a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt +++ b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt @@ -10,9 +10,9 @@ import net.corda.core.node.CityDatabase import net.corda.core.node.WorldMapLocation import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.containsType -import net.corda.core.utilities.DUMMY_MAP -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.core.utilities.DUMMY_REGULATOR +import net.corda.testing.DUMMY_MAP +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.DUMMY_REGULATOR import net.corda.core.utilities.ProgressTracker import net.corda.irs.api.NodeInterestRates import net.corda.node.services.config.NodeConfiguration diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt index 4a0a5632f9..8abca6e424 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt @@ -3,8 +3,8 @@ package net.corda.notarydemo import com.google.common.net.HostAndPort import net.corda.core.div import net.corda.core.node.services.ServiceInfo -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.BOB +import net.corda.testing.ALICE +import net.corda.testing.BOB import net.corda.demorun.util.* import net.corda.demorun.runNodes import net.corda.node.services.transactions.BFTNonValidatingNotaryService diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt index 0a3b416022..f85c9846b2 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt @@ -11,7 +11,7 @@ import net.corda.core.map import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.startFlow import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.BOB +import net.corda.testing.BOB import net.corda.notarydemo.flows.DummyIssueAndMove import net.corda.notarydemo.flows.RPCStartableNotaryFlowClient import kotlin.streams.asSequence diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt index 7c2dd027bf..1915d9ff69 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt @@ -4,9 +4,9 @@ import com.google.common.net.HostAndPort import net.corda.core.crypto.appendToCommonName import net.corda.core.div import net.corda.core.node.services.ServiceInfo -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.BOB -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.ALICE +import net.corda.testing.BOB +import net.corda.testing.DUMMY_NOTARY import net.corda.demorun.util.* import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.node.utilities.ServiceIdentityGenerator diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt index fcd2165253..bdd1c21093 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt @@ -2,9 +2,9 @@ package net.corda.notarydemo import net.corda.core.div import net.corda.core.node.services.ServiceInfo -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.BOB -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.ALICE +import net.corda.testing.BOB +import net.corda.testing.DUMMY_NOTARY import net.corda.demorun.runNodes import net.corda.node.services.startFlowPermission import net.corda.node.services.transactions.ValidatingNotaryService diff --git a/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt b/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt index 171d373603..ec255ccd88 100644 --- a/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt +++ b/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt @@ -4,9 +4,9 @@ import com.google.common.util.concurrent.Futures import com.opengamma.strata.product.common.BuySell import net.corda.core.getOrThrow import net.corda.core.node.services.ServiceInfo -import net.corda.core.utilities.DUMMY_BANK_A -import net.corda.core.utilities.DUMMY_BANK_B -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_BANK_A +import net.corda.testing.DUMMY_BANK_B +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.driver.driver import net.corda.node.services.transactions.SimpleNotaryService import net.corda.testing.IntegrationTestCategory diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApi.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApi.kt index 9412b62262..38ec9efa02 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApi.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApi.kt @@ -12,8 +12,7 @@ import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.startFlow -import net.corda.core.utilities.DUMMY_MAP -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.core.node.services.ServiceType import net.corda.vega.analytics.InitialMarginTriple import net.corda.vega.contracts.IRSState import net.corda.vega.contracts.PortfolioState @@ -33,7 +32,6 @@ import javax.ws.rs.core.Response //TODO: Change import namespaces vega -> .... - @Path("simmvaluationdemo") class PortfolioApi(val rpc: CordaRPCOps) { private val ownParty: Party get() = rpc.nodeIdentity().legalIdentity @@ -228,7 +226,7 @@ class PortfolioApi(val rpc: CordaRPCOps) { return withParty(partyName) { party -> withPortfolio(party) { state -> if (state.valuation != null) { - val isValuer = state.valuer as AbstractParty == ownParty + val isValuer = state.valuer == ownParty val rawMtm = state.valuation.presentValues.map { it.value.amounts.first().amount }.reduce { a, b -> a + b } @@ -254,12 +252,11 @@ class PortfolioApi(val rpc: CordaRPCOps) { @Path("whoami") @Produces(MediaType.APPLICATION_JSON) fun getWhoAmI(): AvailableParties { - val (parties, partyUpdates) = rpc.networkMapUpdates() + val (parties, partyUpdates) = rpc.networkMapFeed() partyUpdates.notUsed() - val counterParties = parties.filter { - it.legalIdentity.name != DUMMY_MAP.name - && it.legalIdentity.name != DUMMY_NOTARY.name - && it.legalIdentity.name != ownParty.name + val counterParties = parties.filterNot { + it.advertisedServices.any { it.info.type in setOf(ServiceType.networkMap, ServiceType.notary) } + || it.legalIdentity == ownParty } return AvailableParties( @@ -280,8 +277,6 @@ class PortfolioApi(val rpc: CordaRPCOps) { return withParty(partyName) { otherParty -> val existingSwap = getPortfolioWith(otherParty) val flowHandle = if (existingSwap == null) { - // TODO: Remove this suppress when we upgrade to kotlin 1.1 or when JetBrain fixes the bug. - @Suppress("UNSUPPORTED_FEATURE") rpc.startFlow(SimmFlow::Requester, otherParty, params.valuationDate) } else { rpc.startFlow(SimmRevaluation::Initiator, getPortfolioStateAndRefWith(otherParty).ref, params.valuationDate) @@ -295,4 +290,3 @@ class PortfolioApi(val rpc: CordaRPCOps) { } } } - diff --git a/samples/simm-valuation-demo/src/test/kotlin/net/corda/vega/Main.kt b/samples/simm-valuation-demo/src/test/kotlin/net/corda/vega/Main.kt index 3410f4ea48..b8a52397ee 100644 --- a/samples/simm-valuation-demo/src/test/kotlin/net/corda/vega/Main.kt +++ b/samples/simm-valuation-demo/src/test/kotlin/net/corda/vega/Main.kt @@ -3,10 +3,10 @@ package net.corda.vega import com.google.common.util.concurrent.Futures import net.corda.core.getOrThrow import net.corda.core.node.services.ServiceInfo -import net.corda.core.utilities.DUMMY_BANK_A -import net.corda.core.utilities.DUMMY_BANK_B -import net.corda.core.utilities.DUMMY_BANK_C -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_BANK_A +import net.corda.testing.DUMMY_BANK_B +import net.corda.testing.DUMMY_BANK_C +import net.corda.testing.DUMMY_NOTARY import net.corda.node.services.transactions.SimpleNotaryService import net.corda.testing.driver.driver diff --git a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt index 4c1507159a..a0198451a9 100644 --- a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt +++ b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt @@ -6,9 +6,9 @@ import net.corda.core.contracts.DOLLARS import net.corda.core.getOrThrow import net.corda.core.millis import net.corda.core.node.services.ServiceInfo -import net.corda.core.utilities.DUMMY_BANK_A -import net.corda.core.utilities.DUMMY_BANK_B -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_BANK_A +import net.corda.testing.DUMMY_BANK_B +import net.corda.testing.DUMMY_NOTARY import net.corda.flows.IssuerFlow import net.corda.testing.driver.poll import net.corda.node.services.startFlowPermission diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemo.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemo.kt index 82a0ab081c..bc84fdb459 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemo.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemo.kt @@ -4,10 +4,8 @@ import com.google.common.net.HostAndPort import joptsimple.OptionParser import net.corda.client.rpc.CordaRPCClient import net.corda.core.contracts.DOLLARS -import net.corda.core.crypto.X509Utilities -import net.corda.core.utilities.DUMMY_BANK_A +import net.corda.testing.DUMMY_BANK_A import net.corda.core.utilities.loggerFor -import org.bouncycastle.asn1.x500.X500Name import org.slf4j.Logger import kotlin.system.exitProcess diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt index caba7d5252..68bb39825d 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt @@ -4,7 +4,7 @@ import com.google.common.util.concurrent.Futures import net.corda.client.rpc.notUsed import net.corda.contracts.CommercialPaper import net.corda.contracts.asset.Cash -import net.corda.contracts.testing.calculateRandomlySizedAmounts +import net.corda.testing.contracts.calculateRandomlySizedAmounts import net.corda.core.contracts.Amount import net.corda.core.contracts.DOLLARS import net.corda.core.contracts.USD diff --git a/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/Main.kt b/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/Main.kt index af4638e205..544055d907 100644 --- a/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/Main.kt +++ b/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/Main.kt @@ -2,9 +2,9 @@ package net.corda.traderdemo import net.corda.core.div import net.corda.core.node.services.ServiceInfo -import net.corda.core.utilities.DUMMY_BANK_A -import net.corda.core.utilities.DUMMY_BANK_B -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_BANK_A +import net.corda.testing.DUMMY_BANK_B +import net.corda.testing.DUMMY_NOTARY import net.corda.flows.IssuerFlow import net.corda.node.services.startFlowPermission import net.corda.node.services.transactions.SimpleNotaryService diff --git a/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeConfig.kt b/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeConfig.kt index 338c88d656..0df543cd5c 100644 --- a/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeConfig.kt +++ b/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeConfig.kt @@ -6,35 +6,35 @@ import com.typesafe.config.ConfigRenderOptions import com.typesafe.config.ConfigValue import com.typesafe.config.ConfigValueFactory import net.corda.core.crypto.commonName -import net.corda.core.identity.Party import net.corda.nodeapi.User +import org.bouncycastle.asn1.x500.X500Name class NodeConfig( - val party: Party, - val p2pPort: Int, - val rpcPort: Int, - val webPort: Int, - val extraServices: List, - val users: List, - var networkMap: NodeConfig? = null + val legalName: X500Name, + val p2pPort: Int, + val rpcPort: Int, + val webPort: Int, + val extraServices: List, + val users: List, + var networkMap: NodeConfig? = null ) { companion object { val renderOptions: ConfigRenderOptions = ConfigRenderOptions.defaults().setOriginComments(false) } - val commonName: String get() = party.name.commonName + val commonName: String get() = legalName.commonName /* * The configuration object depends upon the networkMap, * which is mutable. */ fun toFileConfig(): Config = empty() - .withValue("myLegalName", valueFor(party.name.toString())) + .withValue("myLegalName", valueFor(legalName.toString())) .withValue("p2pAddress", addressValueFor(p2pPort)) .withValue("extraAdvertisedServiceIds", valueFor(extraServices)) .withFallback(optional("networkMapService", networkMap, { c, n -> c.withValue("address", addressValueFor(n.p2pPort)) - .withValue("legalName", valueFor(n.party.name.toString())) + .withValue("legalName", valueFor(n.legalName.toString())) })) .withValue("webAddress", addressValueFor(webPort)) .withValue("rpcAddress", addressValueFor(rpcPort)) diff --git a/test-utils/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt b/test-utils/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt index 77ef496a0c..a6ba60fc47 100644 --- a/test-utils/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt +++ b/test-utils/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt @@ -6,13 +6,12 @@ import net.corda.core.getOrThrow import net.corda.core.list import net.corda.core.node.services.ServiceInfo import net.corda.core.readLines -import net.corda.core.utilities.DUMMY_BANK_A -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.core.utilities.DUMMY_REGULATOR +import net.corda.testing.DUMMY_BANK_A +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.DUMMY_REGULATOR import net.corda.node.internal.NodeStartup import net.corda.node.services.api.RegulatorService import net.corda.node.services.transactions.SimpleNotaryService -import net.corda.nodeapi.ArtemisMessagingComponent import net.corda.testing.ProjectStructure.projectRootDir import org.assertj.core.api.Assertions.assertThat import org.junit.Test diff --git a/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt b/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt index c550aae6d3..b621e2aff0 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt @@ -7,10 +7,6 @@ import com.google.common.net.HostAndPort import com.nhaarman.mockito_kotlin.spy import com.nhaarman.mockito_kotlin.whenever import net.corda.core.contracts.StateRef -import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.X509Utilities -import net.corda.core.crypto.commonName -import net.corda.core.crypto.generateKeyPair import net.corda.core.crypto.* import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate @@ -19,8 +15,9 @@ import net.corda.core.node.VersionInfo import net.corda.core.node.services.IdentityService import net.corda.core.serialization.OpaqueBytes import net.corda.core.transactions.TransactionBuilder -import net.corda.core.utilities.* -import net.corda.node.services.config.* +import net.corda.node.services.config.NodeConfiguration +import net.corda.node.services.config.VerifierType +import net.corda.node.services.config.configureDevKeyAndTrustStores import net.corda.node.services.identity.InMemoryIdentityService import net.corda.nodeapi.config.SSLConfiguration import net.corda.testing.node.MockServices @@ -33,6 +30,7 @@ import java.nio.file.Files import java.nio.file.Path import java.security.KeyPair import java.security.PublicKey +import java.security.cert.CertificateFactory import java.util.concurrent.atomic.AtomicInteger /** @@ -195,3 +193,17 @@ fun getTestX509Name(commonName: String): X500Name { nameBuilder.addRDN(BCStyle.C, "US") return nameBuilder.build() } + +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) +} + +/** + * Build a test party with a nonsense certificate authority for testing purposes. + */ +fun getTestPartyAndCertificate(name: X500Name, publicKey: PublicKey, trustRoot: CertificateAndKeyPair = DUMMY_CA): PartyAndCertificate { + return getTestPartyAndCertificate(Party(name, publicKey), trustRoot) +} \ No newline at end of file diff --git a/test-utils/src/main/kotlin/net/corda/testing/LedgerDSLInterpreter.kt b/test-utils/src/main/kotlin/net/corda/testing/LedgerDSLInterpreter.kt index f68864dfb7..c3fb15115e 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/LedgerDSLInterpreter.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/LedgerDSLInterpreter.kt @@ -6,7 +6,6 @@ import net.corda.core.contracts.TransactionState import net.corda.core.crypto.SecureHash import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.DUMMY_NOTARY import java.io.InputStream /** diff --git a/core/src/main/kotlin/net/corda/core/utilities/TestConstants.kt b/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt similarity index 81% rename from core/src/main/kotlin/net/corda/core/utilities/TestConstants.kt rename to test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt index b42a903493..d2ebdae030 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/TestConstants.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt @@ -1,6 +1,6 @@ @file:JvmName("TestConstants") -package net.corda.core.utilities +package net.corda.testing import net.corda.core.crypto.* import net.corda.core.crypto.testing.DummyPublicKey @@ -10,7 +10,6 @@ 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. @@ -68,14 +67,3 @@ val DUMMY_CA: CertificateAndKeyPair by lazy { 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/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt b/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt index d3ea0a1788..bb660ac058 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt @@ -6,7 +6,6 @@ import net.corda.core.crypto.SecureHash import net.corda.core.identity.Party import net.corda.core.seconds import net.corda.core.transactions.TransactionBuilder -import net.corda.core.utilities.DUMMY_NOTARY import java.security.PublicKey import java.time.Duration import java.time.Instant diff --git a/finance/src/main/kotlin/net/corda/contracts/testing/VaultFiller.kt b/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt similarity index 92% rename from finance/src/main/kotlin/net/corda/contracts/testing/VaultFiller.kt rename to test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt index 923a545d52..7e5382f1e8 100644 --- a/finance/src/main/kotlin/net/corda/contracts/testing/VaultFiller.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt @@ -1,6 +1,6 @@ @file:JvmName("VaultFiller") -package net.corda.contracts.testing +package net.corda.testing.contracts import net.corda.contracts.Commodity import net.corda.contracts.DealState @@ -15,9 +15,9 @@ import net.corda.core.node.ServiceHub import net.corda.core.node.services.Vault import net.corda.core.serialization.OpaqueBytes import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.CHARLIE -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.core.utilities.DUMMY_NOTARY_KEY +import net.corda.testing.CHARLIE +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY_KEY import java.security.KeyPair import java.security.PublicKey import java.time.Instant @@ -63,12 +63,13 @@ fun ServiceHub.fillWithSomeTestLinearStates(numberToCreate: Int, val transactions: List = (1..numberToCreate).map { // Issue a Linear state val dummyIssue = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { - addOutputState(DummyLinearContract.State(linearId = UniqueIdentifier(externalId), - participants = participants.plus(me), - linearString = linearString, - linearNumber = linearNumber, - linearBoolean = linearBoolean, - linearTimestamp = linearTimestamp)) + addOutputState(DummyLinearContract.State( + linearId = UniqueIdentifier(externalId), + participants = participants.plus(me), + linearString = linearString, + linearNumber = linearNumber, + linearBoolean = linearBoolean, + linearTimestamp = linearTimestamp)) signWith(DUMMY_NOTARY_KEY) } @@ -202,7 +203,7 @@ fun ServiceHub.consumeAndProduce(stateAndRef: StateAndRef): // Create a txn consuming different contract types val producedTx = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { addOutputState(DummyLinearContract.State(linearId = stateAndRef.state.data.linearId, - participants = stateAndRef.state.data.participants)) + participants = stateAndRef.state.data.participants)) signWith(DUMMY_NOTARY_KEY) }.toSignedTransaction() diff --git a/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt b/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt index e90b25a655..36d496d187 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -34,7 +34,7 @@ import net.corda.nodeapi.User import net.corda.nodeapi.config.SSLConfiguration import net.corda.nodeapi.config.parseAs import net.corda.nodeapi.internal.addShutdownHook -import net.corda.testing.MOCK_VERSION_INFO +import net.corda.testing.* import okhttp3.OkHttpClient import okhttp3.Request import org.bouncycastle.asn1.x500.X500Name diff --git a/test-utils/src/main/kotlin/net/corda/testing/driver/NetworkMapStartStrategy.kt b/test-utils/src/main/kotlin/net/corda/testing/driver/NetworkMapStartStrategy.kt index 93a42686d4..62248682ef 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/driver/NetworkMapStartStrategy.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/driver/NetworkMapStartStrategy.kt @@ -1,7 +1,7 @@ package net.corda.testing.driver import com.google.common.net.HostAndPort -import net.corda.core.utilities.DUMMY_MAP +import net.corda.testing.DUMMY_MAP import org.bouncycastle.asn1.x500.X500Name sealed class NetworkMapStartStrategy { diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt index ce33be8a44..36a72e77f0 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt @@ -7,9 +7,9 @@ import net.corda.core.identity.Party import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub import net.corda.core.node.services.NetworkMapCache -import net.corda.core.utilities.getTestPartyAndCertificate import net.corda.node.services.network.InMemoryNetworkMapCache import net.corda.testing.MOCK_VERSION_INFO +import net.corda.testing.getTestPartyAndCertificate import net.corda.testing.getTestX509Name import rx.Observable import rx.subjects.PublishSubject diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt index 0d28e98807..2940891179 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -20,8 +20,6 @@ import net.corda.core.node.WorldMapLocation import net.corda.core.node.services.IdentityService import net.corda.core.node.services.KeyManagementService import net.corda.core.node.services.ServiceInfo -import net.corda.core.utilities.DUMMY_NOTARY_KEY -import net.corda.core.utilities.getTestPartyAndCertificate import net.corda.core.utilities.loggerFor import net.corda.flows.TransactionKeyFlow import net.corda.node.internal.AbstractNode @@ -36,14 +34,13 @@ import net.corda.node.services.transactions.SimpleNotaryService import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor -import net.corda.testing.MOCK_VERSION_INFO -import net.corda.testing.getTestX509Name -import net.corda.testing.testNodeConfiguration +import net.corda.testing.* import org.apache.activemq.artemis.utils.ReusableLatch import org.bouncycastle.asn1.x500.X500Name import org.slf4j.Logger import java.math.BigInteger import java.nio.file.FileSystem +import java.nio.file.Path import java.security.KeyPair import java.security.cert.X509Certificate import java.util.* @@ -308,7 +305,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, return node } - fun baseDirectory(nodeId: Int) = filesystem.getPath("/nodes/$nodeId") + fun baseDirectory(nodeId: Int): Path = filesystem.getPath("/nodes/$nodeId") /** * Asks every node in order to process any queued up inbound messages. This may in turn result in nodes diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt index 4584f36316..5422973d68 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -12,8 +12,7 @@ import net.corda.core.node.services.* import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.DUMMY_CA -import net.corda.core.utilities.getTestPartyAndCertificate +import net.corda.testing.DUMMY_CA import net.corda.flows.AnonymisedIdentity import net.corda.node.services.api.StateMachineRecordedTransactionMappingStorage import net.corda.node.services.api.WritableTransactionStorage @@ -29,6 +28,7 @@ import net.corda.node.services.vault.NodeVaultService import net.corda.testing.MEGA_CORP import net.corda.testing.MOCK_IDENTITIES import net.corda.testing.MOCK_VERSION_INFO +import net.corda.testing.getTestPartyAndCertificate import org.bouncycastle.operator.ContentSigner import rx.Observable import rx.subjects.PublishSubject diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt b/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt index cadcc93871..f8f71c8277 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt @@ -9,7 +9,7 @@ import net.corda.core.crypto.appendToCommonName import net.corda.core.crypto.commonName import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceType -import net.corda.core.utilities.DUMMY_MAP +import net.corda.testing.DUMMY_MAP import net.corda.core.utilities.WHITESPACE import net.corda.node.internal.Node import net.corda.node.serialization.NodeClock diff --git a/tools/demobench/build.gradle b/tools/demobench/build.gradle index 18c12a3b62..8d71db2137 100644 --- a/tools/demobench/build.gradle +++ b/tools/demobench/build.gradle @@ -66,7 +66,7 @@ dependencies { compile ':terminal-331a005d6793e52cefc9e2cec6774e62d5a546b1' compile ':pty4j-0.7.2' - testCompile project(':node') + testCompile project(':test-utils') testCompile project(':webserver') testCompile "junit:junit:$junit_version" diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt index 6d9b04d3e5..06fcfad0de 100644 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt @@ -7,7 +7,7 @@ import com.google.common.net.HostAndPort import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigValueFactory import net.corda.core.div -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY import net.corda.node.internal.NetworkMapInfo import net.corda.node.services.config.FullNodeConfiguration import net.corda.nodeapi.User diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt index bf7f7e57a3..6c77fa3b05 100644 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt @@ -1,7 +1,7 @@ package net.corda.demobench.model import net.corda.core.crypto.X509Utilities.getX509Name -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY import net.corda.nodeapi.User import org.junit.Test import java.nio.file.Path diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt index 6a310ab4a9..329086e2c1 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt @@ -18,16 +18,16 @@ import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceType import net.corda.core.serialization.OpaqueBytes import net.corda.core.success -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.BOB -import net.corda.core.utilities.DUMMY_NOTARY import net.corda.flows.* -import net.corda.testing.driver.NodeHandle -import net.corda.testing.driver.PortAllocation -import net.corda.testing.driver.driver import net.corda.node.services.startFlowPermission import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User +import net.corda.testing.ALICE +import net.corda.testing.BOB +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.driver.NodeHandle +import net.corda.testing.driver.PortAllocation +import net.corda.testing.driver.driver import org.bouncycastle.asn1.x500.X500Name import java.time.Instant import java.util.* @@ -133,8 +133,7 @@ class ExplorerSimulation(val options: OptionSet) { // Log to logger when flow finish. fun FlowHandle.log(seq: Int, name: String) { val out = "[$seq] $name $id :" - returnValue.success { - val (stx, idenities) = it + returnValue.success { (stx) -> Main.log.info("$out ${stx.id} ${(stx.tx.outputs.first().data as Cash.State).amount}") }.failure { Main.log.info("$out ${it.message}") diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt index 3ab800aaae..bdbf03aeb9 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt @@ -9,8 +9,8 @@ import net.corda.core.node.services.ServiceInfo import net.corda.core.serialization.OpaqueBytes import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.ALICE +import net.corda.testing.DUMMY_NOTARY import net.corda.flows.CashIssueFlow import net.corda.flows.CashPaymentFlow import net.corda.testing.driver.NetworkMapStartStrategy diff --git a/webserver/src/integration-test/kotlin/net/corda/webserver/WebserverDriverTests.kt b/webserver/src/integration-test/kotlin/net/corda/webserver/WebserverDriverTests.kt index dbbab3e2df..446545944c 100644 --- a/webserver/src/integration-test/kotlin/net/corda/webserver/WebserverDriverTests.kt +++ b/webserver/src/integration-test/kotlin/net/corda/webserver/WebserverDriverTests.kt @@ -2,7 +2,7 @@ package net.corda.webserver import com.google.common.net.HostAndPort import net.corda.core.getOrThrow -import net.corda.core.utilities.DUMMY_BANK_A +import net.corda.testing.DUMMY_BANK_A import net.corda.testing.driver.WebserverHandle import net.corda.testing.driver.addressMustBeBound import net.corda.testing.driver.addressMustNotBeBound From 9e563f9b98b79a308d68ecb01c80ce61df048310 Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Wed, 5 Jul 2017 12:16:47 +0100 Subject: [PATCH 52/97] Add a doc on writing a custom notary --- .../net/corda/docs/CustomNotaryTutorial.kt | 89 +++++++++++++++++++ docs/source/tutorial-custom-notary.rst | 32 +++++++ 2 files changed, 121 insertions(+) create mode 100644 docs/source/example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt create mode 100644 docs/source/tutorial-custom-notary.rst diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt new file mode 100644 index 0000000000..f5ec3116a4 --- /dev/null +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt @@ -0,0 +1,89 @@ +package net.corda.notarydemo + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.TransactionVerificationException +import net.corda.core.flows.FlowLogic +import net.corda.core.identity.Party +import net.corda.core.node.PluginServiceHub +import net.corda.core.node.services.CordaService +import net.corda.core.node.services.TimeWindowChecker +import net.corda.core.node.services.TrustedAuthorityNotaryService +import net.corda.core.transactions.LedgerTransaction +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.WireTransaction +import net.corda.core.utilities.unwrap +import net.corda.flows.* +import net.corda.node.services.transactions.PersistentUniquenessProvider +import net.corda.node.services.transactions.ValidatingNotaryService +import java.security.SignatureException + +// START 1 +@CordaService +class MyCustomValidatingNotaryService(override val services: PluginServiceHub) : TrustedAuthorityNotaryService() { + companion object { + val type = ValidatingNotaryService.type.getSubType("mycustom") + } + + override val timeWindowChecker = TimeWindowChecker(services.clock) + override val uniquenessProvider = PersistentUniquenessProvider() + + override fun createServiceFlow(otherParty: Party, platformVersion: Int): FlowLogic { + return MyValidatingNotaryFlow(otherParty, this) + } + + override fun start() {} + override fun stop() {} +} +// END 1 + +// START 2 +class MyValidatingNotaryFlow(otherSide: Party, service: MyCustomValidatingNotaryService) : NotaryFlow.Service(otherSide, service) { + /** + * The received transaction is checked for contract-validity, which requires fully resolving it into a + * [TransactionForVerification], for which the caller also has to to reveal the whole transaction + * dependency chain. + */ + @Suspendable + override fun receiveAndVerifyTx(): TransactionParts { + val stx = receive(otherSide).unwrap { it } + checkSignatures(stx) + val wtx = stx.tx + validateTransaction(wtx) + val ltx = validateTransaction(wtx) + processTransaction(ltx) + + return TransactionParts(wtx.id, wtx.inputs, wtx.timeWindow) + } + + fun processTransaction(ltx: LedgerTransaction) { + // Add custom transaction processing logic here + } + + private fun checkSignatures(stx: SignedTransaction) { + try { + stx.verifySignatures(serviceHub.myInfo.notaryIdentity.owningKey) + } catch(e: SignedTransaction.SignaturesMissingException) { + throw NotaryException(NotaryError.SignaturesMissing(e)) + } + } + + @Suspendable + fun validateTransaction(wtx: WireTransaction): LedgerTransaction { + try { + resolveTransaction(wtx) + val ltx = wtx.toLedgerTransaction(serviceHub) + ltx.verify() + return ltx + } catch (e: Exception) { + throw when (e) { + is TransactionVerificationException -> NotaryException(NotaryError.TransactionInvalid(e.toString())) + is SignatureException -> NotaryException(NotaryError.SignaturesInvalid(e.toString())) + else -> e + } + } + } + + @Suspendable + private fun resolveTransaction(wtx: WireTransaction) = subFlow(ResolveTransactionsFlow(wtx, otherSide)) +} +// END 2 diff --git a/docs/source/tutorial-custom-notary.rst b/docs/source/tutorial-custom-notary.rst new file mode 100644 index 0000000000..07be4fab06 --- /dev/null +++ b/docs/source/tutorial-custom-notary.rst @@ -0,0 +1,32 @@ +.. highlight:: kotlin + +Writing a custom notary service +=============================== + +.. warning:: Customising a notary service is an advanced feature and not recommended for most use-cases. Currently, + customising Raft or BFT notaries is not yet fully supported. If you want to write your own Raft notary you will have to + implement a custom database connector (or use a separate database for the notary), and use a custom configuration file. + +Similarly to writing an oracle service, the first step is to create a service class in your CorDapp and annotate it +with ``@CordaService``. The Corda node scans for any class with this annotation and initialises them. The only requirement +is that the class provide a constructor with a single parameter of type ``PluginServiceHub``. + +.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt + :language: kotlin + :start-after: START 1 + :end-before: END 1 + +The next step is to write a notary service flow. You are free to copy and modify the existing built-in flows such +as ``ValidatingNotaryFlow``, ``NonValidatingNotaryFlow``, or implement your own from scratch (following the +``NotaryFlow.Service`` template). Below is an example of a custom flow for a *validating* notary service: + +.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt + :language: kotlin + :start-after: START 2 + :end-before: END 2 + +To ensure the custom notary is installed and advertised by the node, specify it in the configuration file: + +.. parsed-literal:: + + extraAdvertisedServiceIds : ["corda.notary.validating.mycustom"] From 32543021c92393767d55556ea129c3c6536ef0d9 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Wed, 5 Jul 2017 14:26:03 +0100 Subject: [PATCH 53/97] Review comments and fix warnings --- .../serialization/carpenter/ClassCarpenter.kt | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/serialization/carpenter/ClassCarpenter.kt b/core/src/main/kotlin/net/corda/core/serialization/carpenter/ClassCarpenter.kt index fde7632034..5a2819b0f4 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/carpenter/ClassCarpenter.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/carpenter/ClassCarpenter.kt @@ -16,6 +16,7 @@ interface SimpleFieldAccess { operator fun get(name: String): Any? } + /** * A class carpenter generates JVM bytecodes for a class given a schema and then loads it into a sub-classloader. * The generated classes have getters, a toString method and implement a simple property access interface. The @@ -60,9 +61,6 @@ interface SimpleFieldAccess { * * Equals/hashCode methods are not yet supported. */ - -fun Map.descriptors() = LinkedHashMap(this.mapValues { it.value.descriptor }) - class ClassCarpenter { // TODO: Generics. // TODO: Sandbox the generated code when a security manager is in use. @@ -70,7 +68,6 @@ class ClassCarpenter { // TODO: Support annotations. // TODO: isFoo getter patterns for booleans (this is what Kotlin generates) - class DuplicateName : RuntimeException("An attempt was made to register two classes with the same name within the same ClassCarpenter namespace.") class InterfaceMismatch(msg: String) : RuntimeException(msg) class NullablePrimitive(msg: String) : RuntimeException(msg) @@ -110,7 +107,7 @@ class ClassCarpenter { } class NonNullableField(field: Class) : Field(field) { - override val nullabilityAnnotation = "Lorg/jetbrains/annotations/Nullable;" + override val nullabilityAnnotation = "Ljavax/annotations/Nullable;" constructor(name: String, field: Class) : this(field) { this.name = name @@ -138,7 +135,7 @@ class ClassCarpenter { class NullableField(field: Class) : Field(field) { - override val nullabilityAnnotation = "Lorg/jetbrains/annotations/NotNull;" + override val nullabilityAnnotation = "Ljavax/annotations/NotNull;" constructor(name: String, field: Class) : this(field) { if (field.isPrimitive) { @@ -163,7 +160,11 @@ class ClassCarpenter { val name: String, fields: Map, val superclass: Schema? = null, - val interfaces: List> = emptyList()) { + val interfaces: List> = emptyList()) + { + private fun Map.descriptors() = + LinkedHashMap(this.mapValues { it.value.descriptor }) + /* Fix the order up front if the user didn't, inject the name into the field as it's neater when iterating */ val fields = LinkedHashMap(fields.mapValues { it.value.copy(it.key, it.value.field) }) @@ -233,8 +234,8 @@ class ClassCarpenter { return _loaded[schema.name]!! } - private fun generateInterface(schema: Schema): Class<*> { - return generate(schema) { cw, schema -> + private fun generateInterface(interfaceSchema: Schema): Class<*> { + return generate(interfaceSchema) { cw, schema -> val interfaces = schema.interfaces.map { it.name.jvm }.toTypedArray() with(cw) { @@ -247,8 +248,8 @@ class ClassCarpenter { } } - private fun generateClass(schema: Schema): Class<*> { - return generate(schema) { cw, schema -> + private fun generateClass(classSchema: Schema): Class<*> { + return generate(classSchema) { cw, schema -> val superName = schema.superclass?.jvmName ?: "java/lang/Object" val interfaces = arrayOf(SimpleFieldAccess::class.java.name.jvm) + schema.interfaces.map { it.name.jvm } From 4e355ba95edd8e4f4446b1ed48d33e693389e381 Mon Sep 17 00:00:00 2001 From: Patrick Kuo Date: Wed, 5 Jul 2017 15:34:04 +0100 Subject: [PATCH 54/97] Add certificate subject name check on node startup (#897) * Add certificate subject name check on node startup * address PR issues --- .../net/corda/node/internal/AbstractNode.kt | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index a01f3b4045..2e754c518c 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -165,12 +165,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, log.warn("Corda node is running in dev mode.") configuration.configureWithDevSSLCertificate() } - require(hasSSLCertificates()) { - "Identity certificate not found. " + - "Please either copy your existing identity key and certificate from another node, " + - "or if you don't have one yet, fill out the config file and run corda.jar --initial-registration. " + - "Read more at: https://docs.corda.net/permissioning.html" - } + validateKeystore() log.info("Node starting up ...") @@ -520,19 +515,30 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, @VisibleForTesting protected open fun acceptableLiveFiberCountOnStop(): Int = 0 - private fun hasSSLCertificates(): Boolean { - val (sslKeystore, keystore) = try { + private fun validateKeystore() { + val containCorrectKeys = try { // This will throw IOException if key file not found or KeyStoreException if keystore password is incorrect. - Pair( - KeyStoreUtilities.loadKeyStore(configuration.sslKeystore, configuration.keyStorePassword), - KeyStoreUtilities.loadKeyStore(configuration.nodeKeystore, configuration.keyStorePassword)) - } catch (e: IOException) { - return false + val sslKeystore = KeyStoreUtilities.loadKeyStore(configuration.sslKeystore, configuration.keyStorePassword) + val identitiesKeystore = KeyStoreUtilities.loadKeyStore(configuration.nodeKeystore, configuration.keyStorePassword) + sslKeystore.containsAlias(X509Utilities.CORDA_CLIENT_TLS) && identitiesKeystore.containsAlias(X509Utilities.CORDA_CLIENT_CA) } catch (e: KeyStoreException) { log.warn("Certificate key store found but key store password does not match configuration.") - return false + false + } catch (e: IOException) { + false + } + require(containCorrectKeys) { + "Identity certificate not found. " + + "Please either copy your existing identity key and certificate from another node, " + + "or if you don't have one yet, fill out the config file and run corda.jar --initial-registration. " + + "Read more at: https://docs.corda.net/permissioning.html" + } + val identitiesKeystore = KeyStoreUtilities.loadKeyStore(configuration.sslKeystore, configuration.keyStorePassword) + val tlsIdentity = identitiesKeystore.getX509Certificate(X509Utilities.CORDA_CLIENT_TLS).subject + + require(tlsIdentity == configuration.myLegalName) { + "Expected '${configuration.myLegalName}' but got '$tlsIdentity' from the keystore." } - return sslKeystore.containsAlias(X509Utilities.CORDA_CLIENT_TLS) && keystore.containsAlias(X509Utilities.CORDA_CLIENT_CA) } // Specific class so that MockNode can catch it. From baaef30d5b32a1fb4cf5f0bf1aee954a70288839 Mon Sep 17 00:00:00 2001 From: Konstantinos Chalkias Date: Wed, 5 Jul 2017 16:14:18 +0100 Subject: [PATCH 55/97] CompositeKey validation checks (#956) --- .../net/corda/core/crypto/CompositeKey.kt | 112 ++++++++++++-- .../corda/core/crypto/CompositeKeyTests.kt | 137 +++++++++++++++++- 2 files changed, 234 insertions(+), 15 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt b/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt index 1e0ae94678..d7d3e8f205 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt @@ -5,6 +5,7 @@ import net.corda.core.serialization.CordaSerializable import org.bouncycastle.asn1.* import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo import java.security.PublicKey +import java.util.* /** * A tree data structure that enables the representation of composite public keys. @@ -28,16 +29,86 @@ class CompositeKey private constructor (val threshold: Int, children: List) : PublicKey { val children = children.sorted() init { - require (children.size == children.toSet().size) { "Trying to construct CompositeKey with duplicated child nodes." } - // If we want PublicKey we only keep one key, otherwise it will lead to semantically equivalent trees but having different structures. - require(children.size > 1) { "Cannot construct CompositeKey with only one child node." } + // TODO: replace with the more extensive, but slower, checkValidity() test. + checkConstraints() + } + + @Transient + private var validated = false + + // Check for key duplication, threshold and weight constraints and test for aggregated weight integer overflow. + private fun checkConstraints() { + require(children.size == children.toSet().size) { "CompositeKey with duplicated child nodes detected." } + // If we want PublicKey we only keep one key, otherwise it will lead to semantically equivalent trees + // but having different structures. + require(children.size > 1) { "CompositeKey must consist of two or more child nodes." } + // We should ensure threshold is positive, because smaller allowable weight for a node key is 1. + require(threshold > 0) { "CompositeKey threshold is set to $threshold, but it should be a positive integer." } + // If threshold is bigger than total weight, then it will never be satisfied. + val totalWeight = totalWeight() + require(threshold <= totalWeight) { "CompositeKey threshold: $threshold cannot be bigger than aggregated weight of " + + "child nodes: $totalWeight"} + } + + // Graph cycle detection in the composite key structure to avoid infinite loops on CompositeKey graph traversal and + // when recursion is used (i.e. in isFulfilledBy()). + // An IdentityHashMap Vs HashMap is used, because a graph cycle causes infinite loop on the CompositeKey.hashCode(). + private fun cycleDetection(visitedMap: IdentityHashMap) { + for ((node) in children) { + if (node is CompositeKey) { + val curVisitedMap = IdentityHashMap() + curVisitedMap.putAll(visitedMap) + require(!curVisitedMap.contains(node)) { "Cycle detected for CompositeKey: $node" } + curVisitedMap.put(node, true) + node.cycleDetection(curVisitedMap) + } + } + } + + /** + * This method will detect graph cycles in the full composite key structure to protect against infinite loops when + * traversing the graph and key duplicates in the each layer. It also checks if the threshold and weight constraint + * requirements are met, while it tests for aggregated-weight integer overflow. + * In practice, this method should be always invoked on the root [CompositeKey], as it inherently + * validates the child nodes (all the way till the leaves). + * TODO: Always call this method when deserialising [CompositeKey]s. + */ + fun checkValidity() { + val visitedMap = IdentityHashMap() + visitedMap.put(this, true) + cycleDetection(visitedMap) // Graph cycle testing on the root node. + checkConstraints() + for ((node, _) in children) { + if (node is CompositeKey) { + // We don't need to check for cycles on the rest of the nodes (testing on the root node is enough). + node.checkConstraints() + } + } + validated = true + } + + // Method to check if the total (aggregated) weight of child nodes overflows. + // Unlike similar solutions that use long conversion, this approach takes advantage of the minimum weight being 1. + private fun totalWeight(): Int { + var sum = 0 + for ((_, weight) in children) { + require (weight > 0) { "Non-positive weight: $weight detected." } + sum = Math.addExact(sum, weight) // Add and check for integer overflow. + } + return sum } /** * Holds node - weight pairs for a CompositeKey. Ordered first by weight, then by node's hashCode. + * Each node should be assigned with a positive weight to avoid certain types of weight underflow attacks. */ @CordaSerializable data class NodeAndWeight(val node: PublicKey, val weight: Int): Comparable, ASN1Object() { + + init { + // We don't allow zero or negative weights. Minimum weight = 1. + require (weight > 0) { "A non-positive weight was detected. Node info: $this" } + } override fun compareTo(other: NodeAndWeight): Int { if (weight == other.weight) { return node.hashCode().compareTo(other.node.hashCode()) @@ -51,6 +122,10 @@ class CompositeKey private constructor (val threshold: Int, vector.add(ASN1Integer(weight.toLong())) return DERSequence(vector) } + + override fun toString(): String { + return "Public key: ${node.toStringShort()}, weight: $weight" + } } companion object { @@ -75,21 +150,30 @@ class CompositeKey private constructor (val threshold: Int, } override fun getFormat() = ASN1Encoding.DER + // Extracted method from isFulfilledBy. + private fun checkFulfilledBy(keysToCheck: Iterable): Boolean { + if (keysToCheck.any { it is CompositeKey } ) return false + val totalWeight = children.map { (node, weight) -> + if (node is CompositeKey) { + if (node.checkFulfilledBy(keysToCheck)) weight else 0 + } else { + if (keysToCheck.contains(node)) weight else 0 + } + }.sum() + return totalWeight >= threshold + } + /** * Function checks if the public keys corresponding to the signatures are matched against the leaves of the composite * key tree in question, and the total combined weight of all children is calculated for every intermediary node. * If all thresholds are satisfied, the composite key requirement is considered to be met. */ fun isFulfilledBy(keysToCheck: Iterable): Boolean { - if (keysToCheck.any { it is CompositeKey } ) return false - val totalWeight = children.map { (node, weight) -> - if (node is CompositeKey) { - if (node.isFulfilledBy(keysToCheck)) weight else 0 - } else { - if (keysToCheck.contains(node)) weight else 0 - } - }.sum() - return totalWeight >= threshold + // We validate keys only when checking if they're matched, as this checks subkeys as a result. + // Doing these checks at deserialization/construction time would result in duplicate checks. + if (!validated) + checkValidity() // TODO: remove when checkValidity() will be eventually invoked during/after deserialization. + return checkFulfilledBy(keysToCheck) } /** @@ -134,14 +218,14 @@ class CompositeKey private constructor (val threshold: Int, /** * Builds the [CompositeKey]. If [threshold] is not specified, it will default to - * the size of the children, effectively generating an "N of N" requirement. + * the total (aggregated) weight of the children, effectively generating an "N of N" requirement. * During process removes single keys wrapped in [CompositeKey] and enforces ordering on child nodes. */ @Throws(IllegalArgumentException::class) fun build(threshold: Int? = null): PublicKey { val n = children.size if (n > 1) - return CompositeKey(threshold ?: n, children) + return CompositeKey(threshold ?: children.map { (_, weight) -> weight }.sum(), children) else if (n == 1) { require(threshold == null || threshold == children.first().weight) { "Trying to build invalid CompositeKey, threshold value different than weight of single child node." } diff --git a/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt b/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt index 8d2bed5a73..9029da4448 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt @@ -4,6 +4,7 @@ import net.corda.core.serialization.OpaqueBytes import net.corda.core.serialization.serialize import org.junit.Test import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertFalse import kotlin.test.assertTrue @@ -21,7 +22,6 @@ class CompositeKeyTests { val aliceSignature = aliceKey.sign(message) val bobSignature = bobKey.sign(message) val charlieSignature = charlieKey.sign(message) - val compositeAliceSignature = CompositeSignaturesWithKeys(listOf(aliceSignature)) @Test fun `(Alice) fulfilled by Alice signature`() { @@ -124,4 +124,139 @@ class CompositeKeyTests { val brokenBobSignature = DigitalSignature.WithKey(bobSignature.by, aliceSignature.bytes) assertFalse { engine.verify(CompositeSignaturesWithKeys(listOf(aliceSignature, brokenBobSignature)).serialize().bytes) } } + + @Test() + fun `composite key constraints`() { + // Zero weight. + assertFailsWith(IllegalArgumentException::class) { + CompositeKey.Builder().addKey(alicePublicKey, 0) + } + // Negative weight. + assertFailsWith(IllegalArgumentException::class) { + CompositeKey.Builder().addKey(alicePublicKey, -1) + } + // Zero threshold. + assertFailsWith(IllegalArgumentException::class) { + CompositeKey.Builder().addKey(alicePublicKey).build(0) + } + // Negative threshold. + assertFailsWith(IllegalArgumentException::class) { + CompositeKey.Builder().addKey(alicePublicKey).build(-1) + } + // Threshold > Total-weight. + assertFailsWith(IllegalArgumentException::class) { + CompositeKey.Builder().addKey(alicePublicKey, 2).addKey(bobPublicKey, 2).build(5) + } + // Threshold value different than weight of single child node. + assertFailsWith(IllegalArgumentException::class) { + CompositeKey.Builder().addKey(alicePublicKey, 3).build(2) + } + // Aggregated weight integer overflow. + assertFailsWith(IllegalArgumentException::class) { + CompositeKey.Builder().addKey(alicePublicKey, Int.MAX_VALUE).addKey(bobPublicKey, Int.MAX_VALUE).build() + } + // Duplicated children. + assertFailsWith(IllegalArgumentException::class) { + CompositeKey.Builder().addKeys(alicePublicKey, bobPublicKey, alicePublicKey).build() + } + // Duplicated composite key children. + assertFailsWith(IllegalArgumentException::class) { + val compositeKey1 = CompositeKey.Builder().addKeys(alicePublicKey, bobPublicKey).build() + val compositeKey2 = CompositeKey.Builder().addKeys(bobPublicKey, alicePublicKey).build() + CompositeKey.Builder().addKeys(compositeKey1, compositeKey2).build() + } + } + + @Test() + fun `composite key validation with graph cycle detection`() { + val key1 = CompositeKey.Builder().addKeys(alicePublicKey, bobPublicKey).build() as CompositeKey + val key2 = CompositeKey.Builder().addKeys(alicePublicKey, key1).build() as CompositeKey + val key3 = CompositeKey.Builder().addKeys(alicePublicKey, key2).build() as CompositeKey + val key4 = CompositeKey.Builder().addKeys(alicePublicKey, key3).build() as CompositeKey + val key5 = CompositeKey.Builder().addKeys(alicePublicKey, key4).build() as CompositeKey + val key6 = CompositeKey.Builder().addKeys(alicePublicKey, key5, key2).build() as CompositeKey + + // Initially, there is no any graph cycle. + key1.checkValidity() + key2.checkValidity() + key3.checkValidity() + key4.checkValidity() + key5.checkValidity() + // The fact that key6 has a direct reference to key2 and an indirect (via path key5->key4->key3->key2) + // does not imply a cycle, as expected (independent paths). + key6.checkValidity() + + // We will create a graph cycle between key5 and key3. Key5 has already a reference to key3 (via key4). + // To create a cycle, we add a reference (child) from key3 to key5. + // Children list is immutable, so reflection is used to inject key5 as an extra NodeAndWeight child of key3. + val field = key3.javaClass.getDeclaredField("children") + field.isAccessible = true + val fixedChildren = key3.children.plus(CompositeKey.NodeAndWeight(key5, 1)) + field.set(key3, fixedChildren) + + /* A view of the example graph cycle. + * + * key6 + * / \ + * key5 key2 + * / + * key4 + * / + * key3 + * / \ + * key2 key5 + * / + * key1 + * + */ + + // Detect the graph cycle starting from key3. + assertFailsWith(IllegalArgumentException::class) { + key3.checkValidity() + } + + // Detect the graph cycle starting from key4. + assertFailsWith(IllegalArgumentException::class) { + key4.checkValidity() + } + + // Detect the graph cycle starting from key5. + assertFailsWith(IllegalArgumentException::class) { + key5.checkValidity() + } + + // Detect the graph cycle starting from key6. + // Typically, one needs to test on the root tree-node only (thus, a validity check on key6 would be enough). + assertFailsWith(IllegalArgumentException::class) { + key6.checkValidity() + } + + // Key2 (and all paths below it, i.e. key1) are outside the graph cycle and thus, there is no impact on them. + key2.checkValidity() + key1.checkValidity() + } + + @Test + fun `CompositeKey from multiple signature schemes and signature verification`() { + val (privRSA, pubRSA) = Crypto.generateKeyPair(Crypto.RSA_SHA256) + val (privK1, pubK1) = Crypto.generateKeyPair(Crypto.ECDSA_SECP256K1_SHA256) + val (privR1, pubR1) = Crypto.generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256) + val (privEd, pubEd) = Crypto.generateKeyPair(Crypto.EDDSA_ED25519_SHA512) + val (privSP, pubSP) = Crypto.generateKeyPair(Crypto.SPHINCS256_SHA256) + + val RSASignature = privRSA.sign(message.bytes, pubRSA) + val K1Signature = privK1.sign(message.bytes, pubK1) + val R1Signature = privR1.sign(message.bytes, pubR1) + val EdSignature = privEd.sign(message.bytes, pubEd) + val SPSignature = privSP.sign(message.bytes, pubSP) + + val compositeKey = CompositeKey.Builder().addKeys(pubRSA, pubK1, pubR1, pubEd, pubSP).build() as CompositeKey + + val signatures = listOf(RSASignature, K1Signature, R1Signature, EdSignature, SPSignature) + assertTrue { compositeKey.isFulfilledBy(signatures.byKeys()) } + + // One signature is missing. + val signaturesWithoutRSA = listOf(K1Signature, R1Signature, EdSignature, SPSignature) + assertFalse { compositeKey.isFulfilledBy(signaturesWithoutRSA.byKeys()) } + } } From 44f57639d2349307705a73dc0a938d9d419fcd36 Mon Sep 17 00:00:00 2001 From: josecoll Date: Thu, 6 Jul 2017 10:57:59 +0100 Subject: [PATCH 56/97] Vault Query Aggregate Function support (#950) * Partial (ie. incomplete) implementation of Aggregate Functions. * Completed implementation of Aggregate Functions (sum, count, max, min, avg) with optional grouping. * Completed Java DSL and associated JUnit tests. * Added optional sorting by aggregate function. * Added Jvm filename annotation on QueryCriteriaUtils. * Added documentation (API and RST with code samples). * Incorporating feedback from MH - improved readability in structuring Java and/or queries. * Remove redundant import. * Removed redundant commas. * Streamlined expression parsing (in doing so, remove the ugly try-catch raised by RP in PR review comments.) * Added JvmStatic and JvmOverloads to Java DSL; removed duplicate Kotlin DSL functions using default params; changed varargs to lists due to ambiguity * Fix missing imports after rebase from master. * Fix errors following rebase from master. * Updates on expression handling following feedback from RP. --- .../net/corda/core/messaging/CordaRPCOps.kt | 2 + .../net/corda/core/node/services/Services.kt | 10 +- .../core/node/services/vault/QueryCriteria.kt | 12 +- .../node/services/vault/QueryCriteriaUtils.kt | 84 ++++++-- docs/source/api-vault-query.rst | 72 ++++++- .../WorkflowTransactionBuildTutorialTest.kt | 1 - .../vault/HibernateQueryCriteriaParser.kt | 79 ++++++-- .../services/vault/HibernateVaultQueryImpl.kt | 22 +- .../services/vault/VaultQueryJavaTests.java | 188 ++++++++++++++++- .../database/HibernateConfigurationTest.kt | 106 ++++++++++ .../node/services/vault/VaultQueryTests.kt | 190 +++++++++++++++++- 11 files changed, 695 insertions(+), 71 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt index 65cf172153..fc5d9d0b4a 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt @@ -80,6 +80,8 @@ interface CordaRPCOps : RPCOps { * 2. states metadata as a List of [Vault.StateMetadata] held in the Vault States table. * 3. the [PageSpecification] used in the query * 4. a total number of results available (for subsequent paging if necessary) + * 5. status types used in this query: UNCONSUMED, CONSUMED, ALL + * 6. other results (aggregate functions with/without using value groups) * * Note: a default [PageSpecification] is applied to the query returning the 1st page (indexed from 0) with up to 200 entries. * It is the responsibility of the Client to request further pages and/or specify a more suitable [PageSpecification]. diff --git a/core/src/main/kotlin/net/corda/core/node/services/Services.kt b/core/src/main/kotlin/net/corda/core/node/services/Services.kt index 4ebfe100a4..d119662533 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/Services.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/Services.kt @@ -122,13 +122,19 @@ class Vault(val states: Iterable>) { * 4) a total number of states that met the given [QueryCriteria] * Note that this may be more than the specified [PageSpecification.pageSize], and should be used to perform * further pagination (by issuing new queries). + * 5) Status types used in this query: UNCONSUMED, CONSUMED, ALL + * 6) Other results as a [List] of any type (eg. aggregate function results with/without group by) + * + * Note: currently otherResults are used only for Aggregate Functions (in which case, the states and statesMetadata + * results will be empty) */ @CordaSerializable data class Page(val states: List>, val statesMetadata: List, val pageable: PageSpecification, val totalStatesAvailable: Int, - val stateTypes: StateStatus) + val stateTypes: StateStatus, + val otherResults: List) @CordaSerializable data class StateMetadata(val ref: StateRef, @@ -349,6 +355,8 @@ interface VaultQueryService { * 2. states metadata as a List of [Vault.StateMetadata] held in the Vault States table. * 3. the [PageSpecification] used in the query * 4. a total number of results available (for subsequent paging if necessary) + * 5. status types used in this query: UNCONSUMED, CONSUMED, ALL + * 6. other results (aggregate functions with/without using value groups) * * @throws VaultQueryException if the query cannot be executed for any reason * (missing criteria or parsing error, invalid operator, unsupported query, underlying database error) diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt index 6677352c6c..4726ef8199 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt @@ -1,3 +1,5 @@ +@file:JvmName("QueryCriteria") + package net.corda.core.node.services.vault import net.corda.core.contracts.ContractState @@ -5,8 +7,6 @@ import net.corda.core.contracts.StateRef import net.corda.core.contracts.UniqueIdentifier import net.corda.core.identity.AbstractParty import net.corda.core.node.services.Vault -import net.corda.core.node.services.vault.QueryCriteria.AndComposition -import net.corda.core.node.services.vault.QueryCriteria.OrComposition import net.corda.core.schemas.PersistentState import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.OpaqueBytes @@ -114,6 +114,9 @@ sealed class QueryCriteria { RECORDED, CONSUMED } + + infix fun and(criteria: QueryCriteria): QueryCriteria = AndComposition(this, criteria) + infix fun or(criteria: QueryCriteria): QueryCriteria = OrComposition(this, criteria) } interface IQueryCriteriaParser { @@ -125,7 +128,4 @@ interface IQueryCriteriaParser { fun parseOr(left: QueryCriteria, right: QueryCriteria): Collection fun parseAnd(left: QueryCriteria, right: QueryCriteria): Collection fun parse(criteria: QueryCriteria, sorting: Sort? = null) : Collection -} - -infix fun QueryCriteria.and(criteria: QueryCriteria): QueryCriteria = AndComposition(this, criteria) -infix fun QueryCriteria.or(criteria: QueryCriteria): QueryCriteria = OrComposition(this, criteria) +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt index 4dcd9e6ed9..69c5e6ce8d 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt @@ -1,3 +1,5 @@ +@file:JvmName("QueryCriteriaUtils") + package net.corda.core.node.services.vault import net.corda.core.schemas.PersistentState @@ -44,11 +46,23 @@ enum class CollectionOperator { NOT_IN } +@CordaSerializable +enum class AggregateFunctionType { + COUNT, + AVG, + MIN, + MAX, + SUM, +} + @CordaSerializable sealed class CriteriaExpression { data class BinaryLogical(val left: CriteriaExpression, val right: CriteriaExpression, val operator: BinaryLogicalOperator) : CriteriaExpression() data class Not(val expression: CriteriaExpression) : CriteriaExpression() data class ColumnPredicateExpression(val column: Column, val predicate: ColumnPredicate) : CriteriaExpression() + data class AggregateFunctionExpression(val column: Column, val predicate: ColumnPredicate, + val groupByColumns: List>?, + val orderBy: Sort.Direction?) : CriteriaExpression() } @CordaSerializable @@ -65,6 +79,7 @@ sealed class ColumnPredicate { data class CollectionExpression(val operator: CollectionOperator, val rightLiteral: Collection) : ColumnPredicate() data class Between>(val rightFromLiteral: C, val rightToLiteral: C) : ColumnPredicate() data class NullExpression(val operator: NullOperator) : ColumnPredicate() + data class AggregateFunction(val type: AggregateFunctionType) : ColumnPredicate() } fun resolveEnclosingObjectFromExpression(expression: CriteriaExpression): Class { @@ -72,6 +87,7 @@ fun resolveEnclosingObjectFromExpression(expression: CriteriaExpression resolveEnclosingObjectFromExpression(expression.left) is CriteriaExpression.Not -> resolveEnclosingObjectFromExpression(expression.expression) is CriteriaExpression.ColumnPredicateExpression -> resolveEnclosingObjectFromColumn(expression.column) + is CriteriaExpression.AggregateFunctionExpression -> resolveEnclosingObjectFromColumn(expression.column) } } @@ -140,14 +156,14 @@ data class Sort(val columns: Collection) { STATE_STATUS("stateStatus"), RECORDED_TIME("recordedTime"), CONSUMED_TIME("consumedTime"), - LOCK_ID("lockId"), + LOCK_ID("lockId") } enum class LinearStateAttribute(val columnName: String) : Attribute { /** Vault Linear States */ UUID("uuid"), EXTERNAL_ID("externalId"), - DEAL_REFERENCE("dealReference"), + DEAL_REFERENCE("dealReference") } enum class FungibleStateAttribute(val columnName: String) : Attribute { @@ -183,10 +199,15 @@ sealed class SortAttribute { object Builder { fun > compare(operator: BinaryComparisonOperator, value: R) = ColumnPredicate.BinaryComparison(operator, value) - fun KProperty1.predicate(predicate: ColumnPredicate) = CriteriaExpression.ColumnPredicateExpression(Column.Kotlin(this), predicate) + fun Field.predicate(predicate: ColumnPredicate) = CriteriaExpression.ColumnPredicateExpression(Column.Java(this), predicate) + fun KProperty1.functionPredicate(predicate: ColumnPredicate, groupByColumns: List>? = null, orderBy: Sort.Direction? = null) + = CriteriaExpression.AggregateFunctionExpression(Column.Kotlin(this), predicate, groupByColumns, orderBy) + fun Field.functionPredicate(predicate: ColumnPredicate, groupByColumns: List>? = null, orderBy: Sort.Direction? = null) + = CriteriaExpression.AggregateFunctionExpression(Column.Java(this), predicate, groupByColumns, orderBy) + fun > KProperty1.comparePredicate(operator: BinaryComparisonOperator, value: R) = predicate(compare(operator, value)) fun > Field.comparePredicate(operator: BinaryComparisonOperator, value: R) = predicate(compare(operator, value)) @@ -200,15 +221,15 @@ object Builder { fun > KProperty1.`in`(collection: Collection) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection)) fun > KProperty1.notIn(collection: Collection) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection)) - fun Field.equal(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value)) - fun Field.notEqual(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value)) - fun > Field.lessThan(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN, value) - fun > Field.lessThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN_OR_EQUAL, value) - fun > Field.greaterThan(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN, value) - fun > Field.greaterThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN_OR_EQUAL, value) - fun > Field.between(from: R, to: R) = predicate(ColumnPredicate.Between(from, to)) - fun > Field.`in`(collection: Collection) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection)) - fun > Field.notIn(collection: Collection) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection)) + @JvmStatic fun Field.equal(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value)) + @JvmStatic fun Field.notEqual(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value)) + @JvmStatic fun > Field.lessThan(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN, value) + @JvmStatic fun > Field.lessThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN_OR_EQUAL, value) + @JvmStatic fun > Field.greaterThan(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN, value) + @JvmStatic fun > Field.greaterThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN_OR_EQUAL, value) + @JvmStatic fun > Field.between(from: R, to: R) = predicate(ColumnPredicate.Between(from, to)) + @JvmStatic fun > Field.`in`(collection: Collection) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection)) + @JvmStatic fun > Field.notIn(collection: Collection) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection)) fun equal(value: R) = ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value) fun notEqual(value: R) = ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value) @@ -221,14 +242,45 @@ object Builder { fun > notIn(collection: Collection) = ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection) fun KProperty1.like(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.LIKE, string)) - fun Field.like(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.LIKE, string)) + @JvmStatic fun Field.like(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.LIKE, string)) fun KProperty1.notLike(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.NOT_LIKE, string)) - fun Field.notLike(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.NOT_LIKE, string)) + @JvmStatic fun Field.notLike(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.NOT_LIKE, string)) fun KProperty1.isNull() = predicate(ColumnPredicate.NullExpression(NullOperator.IS_NULL)) - fun Field.isNull() = predicate(ColumnPredicate.NullExpression(NullOperator.IS_NULL)) + @JvmStatic fun Field.isNull() = predicate(ColumnPredicate.NullExpression(NullOperator.IS_NULL)) fun KProperty1.notNull() = predicate(ColumnPredicate.NullExpression(NullOperator.NOT_NULL)) - fun Field.notNull() = predicate(ColumnPredicate.NullExpression(NullOperator.NOT_NULL)) + @JvmStatic fun Field.notNull() = predicate(ColumnPredicate.NullExpression(NullOperator.NOT_NULL)) + + /** aggregate functions */ + fun KProperty1.sum(groupByColumns: List>? = null, orderBy: Sort.Direction? = null) = + functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.SUM), groupByColumns?.map { Column.Kotlin(it) }, orderBy) + @JvmStatic @JvmOverloads + fun Field.sum(groupByColumns: List? = null, orderBy: Sort.Direction? = null) = + functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.SUM), groupByColumns?.map { Column.Java(it) }, orderBy) + + fun KProperty1.count() = functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.COUNT)) + @JvmStatic fun Field.count() = functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.COUNT)) + + fun KProperty1.avg(groupByColumns: List>? = null, orderBy: Sort.Direction? = null) = + functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.AVG), groupByColumns?.map { Column.Kotlin(it) }, orderBy) + @JvmStatic + @JvmOverloads + fun Field.avg(groupByColumns: List? = null, orderBy: Sort.Direction? = null) = + functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.AVG), groupByColumns?.map { Column.Java(it) }, orderBy) + + fun KProperty1.min(groupByColumns: List>? = null, orderBy: Sort.Direction? = null) = + functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.MIN), groupByColumns?.map { Column.Kotlin(it) }, orderBy) + @JvmStatic + @JvmOverloads + fun Field.min(groupByColumns: List? = null, orderBy: Sort.Direction? = null) = + functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.MIN), groupByColumns?.map { Column.Java(it) }, orderBy) + + fun KProperty1.max(groupByColumns: List>? = null, orderBy: Sort.Direction? = null) = + functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.MAX), groupByColumns?.map { Column.Kotlin(it) }, orderBy) + @JvmStatic + @JvmOverloads + fun Field.max(groupByColumns: List? = null, orderBy: Sort.Direction? = null) = + functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.MAX), groupByColumns?.map { Column.Java(it) }, orderBy) } inline fun builder(block: Builder.() -> A) = block(Builder) diff --git a/docs/source/api-vault-query.rst b/docs/source/api-vault-query.rst index 817318380f..fd31eb9497 100644 --- a/docs/source/api-vault-query.rst +++ b/docs/source/api-vault-query.rst @@ -50,27 +50,27 @@ The API provides both static (snapshot) and dynamic (snapshot with streaming upd Simple pagination (page number and size) and sorting (directional ordering using standard or custom property attributes) is also specifiable. Defaults are defined for Paging (pageNumber = 0, pageSize = 200) and Sorting (direction = ASC). -The ``QueryCriteria`` interface provides a flexible mechanism for specifying different filtering criteria, including and/or composition and a rich set of operators to include: binary logical (AND, OR), comparison (LESS_THAN, LESS_THAN_OR_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL), equality (EQUAL, NOT_EQUAL), likeness (LIKE, NOT_LIKE), nullability (IS_NULL, NOT_NULL), and collection based (IN, NOT_IN). +The ``QueryCriteria`` interface provides a flexible mechanism for specifying different filtering criteria, including and/or composition and a rich set of operators to include: binary logical (AND, OR), comparison (LESS_THAN, LESS_THAN_OR_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL), equality (EQUAL, NOT_EQUAL), likeness (LIKE, NOT_LIKE), nullability (IS_NULL, NOT_NULL), and collection based (IN, NOT_IN). Standard SQL-92 aggregate functions (SUM, AVG, MIN, MAX, COUNT) are also supported. There are four implementations of this interface which can be chained together to define advanced filters. - 1. ``VaultQueryCriteria`` provides filterable criteria on attributes within the Vault states table: status (UNCONSUMED, CONSUMED), state reference(s), contract state type(s), notaries, soft locked states, timestamps (RECORDED, CONSUMED). +1. ``VaultQueryCriteria`` provides filterable criteria on attributes within the Vault states table: status (UNCONSUMED, CONSUMED), state reference(s), contract state type(s), notaries, soft locked states, timestamps (RECORDED, CONSUMED). - .. note:: Sensible defaults are defined for frequently used attributes (status = UNCONSUMED, includeSoftlockedStates = true). + .. note:: Sensible defaults are defined for frequently used attributes (status = UNCONSUMED, includeSoftlockedStates = true). - 2. ``FungibleAssetQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core ``FungibleAsset`` contract state interface, used to represent assets that are fungible, countable and issued by a specific party (eg. ``Cash.State`` and ``CommodityContract.State`` in the Corda finance module). Filterable attributes include: participants(s), owner(s), quantity, issuer party(s) and issuer reference(s). +2. ``FungibleAssetQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core ``FungibleAsset`` contract state interface, used to represent assets that are fungible, countable and issued by a specific party (eg. ``Cash.State`` and ``CommodityContract.State`` in the Corda finance module). Filterable attributes include: participants(s), owner(s), quantity, issuer party(s) and issuer reference(s). - .. note:: All contract states that extend the ``FungibleAsset`` now automatically persist that interfaces common state attributes to the **vault_fungible_states** table. + .. note:: All contract states that extend the ``FungibleAsset`` now automatically persist that interfaces common state attributes to the **vault_fungible_states** table. - 3. ``LinearStateQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core ``LinearState`` and ``DealState`` contract state interfaces, used to represent entities that continuously supercede themselves, all of which share the same *linearId* (eg. trade entity states such as the ``IRSState`` defined in the SIMM valuation demo). Filterable attributes include: participant(s), linearId(s), dealRef(s). +3. ``LinearStateQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core ``LinearState`` and ``DealState`` contract state interfaces, used to represent entities that continuously supercede themselves, all of which share the same *linearId* (eg. trade entity states such as the ``IRSState`` defined in the SIMM valuation demo). Filterable attributes include: participant(s), linearId(s), dealRef(s). - .. note:: All contract states that extend ``LinearState`` or ``DealState`` now automatically persist those interfaces common state attributes to the **vault_linear_states** table. + .. note:: All contract states that extend ``LinearState`` or ``DealState`` now automatically persist those interfaces common state attributes to the **vault_linear_states** table. - 4. ``VaultCustomQueryCriteria`` provides the means to specify one or many arbitrary expressions on attributes defined by a custom contract state that implements its own schema as described in the :doc:`Persistence ` documentation and associated examples. Custom criteria expressions are expressed using one of several type-safe ``CriteriaExpression``: BinaryLogical, Not, ColumnPredicateExpression. The ColumnPredicateExpression allows for specification arbitrary criteria using the previously enumerated operator types. Furthermore, a rich DSL is provided to enable simple construction of custom criteria using any combination of ``ColumnPredicate``. +4. ``VaultCustomQueryCriteria`` provides the means to specify one or many arbitrary expressions on attributes defined by a custom contract state that implements its own schema as described in the :doc:`Persistence ` documentation and associated examples. Custom criteria expressions are expressed using one of several type-safe ``CriteriaExpression``: BinaryLogical, Not, ColumnPredicateExpression, AggregateFunctionExpression. The ``ColumnPredicateExpression`` allows for specification arbitrary criteria using the previously enumerated operator types. The ``AggregateFunctionExpression`` allows for the specification of an aggregate function type (sum, avg, max, min, count) with optional grouping and sorting. Furthermore, a rich DSL is provided to enable simple construction of custom criteria using any combination of ``ColumnPredicate``. See the ``Builder`` object in ``QueryCriteriaUtils`` for a complete specification of the DSL. .. note:: It is a requirement to register any custom contract schemas to be used in Vault Custom queries in the associated `CordaPluginRegistry` configuration for the respective CorDapp using the ``requiredSchemas`` configuration field (which specifies a set of `MappedSchema`) - An example is illustrated here: +An example of a custom query is illustrated here: .. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt :language: kotlin @@ -236,6 +236,37 @@ Query for fungible assets for a specifc issuer party: :start-after: DOCSTART VaultQueryExample14 :end-before: DOCEND VaultQueryExample14 +**Aggregate Function queries using** ``VaultCustomQueryCriteria`` + +.. note:: Query results for aggregate functions are contained in the `otherResults` attribute of a results Page. + +Aggregations on cash using various functions: + +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt + :language: kotlin + :start-after: DOCSTART VaultQueryExample21 + :end-before: DOCEND VaultQueryExample21 + +.. note:: `otherResults` will contain 5 items, one per calculated aggregate function. + +Aggregations on cash grouped by currency for various functions: + +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt + :language: kotlin + :start-after: DOCSTART VaultQueryExample22 + :end-before: DOCEND VaultQueryExample22 + +.. note:: `otherResults` will contain 24 items, one result per calculated aggregate function per currency (the grouping attribute - currency in this case - is returned per aggregate result). + +Sum aggregation on cash grouped by issuer party and currency and sorted by sum: + +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt + :language: kotlin + :start-after: DOCSTART VaultQueryExample23 + :end-before: DOCEND VaultQueryExample23 + +.. note:: `otherResults` will contain 12 items sorted from largest summed cash amount to smallest, one result per calculated aggregate function per issuer party and currency (grouping attributes are returned per aggregate result). + **Dynamic queries** (also using ``VaultQueryCriteria``) are an extension to the snapshot queries by returning an additional ``QueryResults`` return type in the form of an ``Observable``. Refer to `ReactiveX Observable `_ for a detailed understanding and usage of this type. Track unconsumed cash states: @@ -301,6 +332,29 @@ Query for consumed deal states or linear ids, specify a paging specification and :start-after: DOCSTART VaultJavaQueryExample2 :end-before: DOCEND VaultJavaQueryExample2 +**Aggregate Function queries using** ``VaultCustomQueryCriteria`` + +Aggregations on cash using various functions: + +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryJavaTests.kt + :language: kotlin + :start-after: DOCSTART VaultJavaQueryExample21 + :end-before: DOCEND VaultJavaQueryExample21 + +Aggregations on cash grouped by currency for various functions: + +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryJavaTests.kt + :language: kotlin + :start-after: DOCSTART VaultJavaQueryExample22 + :end-before: DOCEND VaultJavaQueryExample22 + +Sum aggregation on cash grouped by issuer party and currency and sorted by sum: + +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryJavaTests.kt + :language: kotlin + :start-after: DOCSTART VaultJavaQueryExample23 + :end-before: DOCEND VaultJavaQueryExample23 + Track unconsumed cash states: .. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt index 2e165793f4..2a819c25cc 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt @@ -9,7 +9,6 @@ import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.Vault import net.corda.core.node.services.queryBy import net.corda.core.node.services.vault.QueryCriteria -import net.corda.core.node.services.vault.and import net.corda.core.toFuture import net.corda.testing.DUMMY_NOTARY import net.corda.testing.DUMMY_NOTARY_KEY diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt index 959eeb73b7..f0456d66c2 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt @@ -35,6 +35,7 @@ class HibernateQueryCriteriaParser(val contractType: Class, private val joinPredicates = mutableListOf() // incrementally build list of root entities (for later use in Sort parsing) private val rootEntities = mutableMapOf, Root<*>>() + private val aggregateExpressions = mutableListOf>() var stateTypes: Vault.StateStatus = Vault.StateStatus.UNCONSUMED @@ -78,7 +79,7 @@ class HibernateQueryCriteriaParser(val contractType: Class, QueryCriteria.TimeInstantType.CONSUMED -> Column.Kotlin(VaultSchemaV1.VaultStates::consumedTime) } val expression = CriteriaExpression.ColumnPredicateExpression(timeColumn, timeCondition.predicate) - predicateSet.add(expressionToPredicate(vaultStates, expression)) + predicateSet.add(parseExpression(vaultStates, expression) as Predicate) } return predicateSet } @@ -127,32 +128,75 @@ class HibernateQueryCriteriaParser(val contractType: Class, NullOperator.NOT_NULL -> criteriaBuilder.isNotNull(column) } } + else -> throw VaultQueryException("Not expecting $columnPredicate") } } - /** - * @return : Expression -> : Predicate - */ - private fun expressionToExpression(root: Root, expression: CriteriaExpression): Expression { + private fun parseExpression(entityRoot: Root, expression: CriteriaExpression, predicateSet: MutableSet) { + if (expression is CriteriaExpression.AggregateFunctionExpression) { + parseAggregateFunction(entityRoot, expression) + } else { + predicateSet.add(parseExpression(entityRoot, expression) as Predicate) + } + } + + private fun parseExpression(root: Root, expression: CriteriaExpression): Expression { return when (expression) { is CriteriaExpression.BinaryLogical -> { - val leftPredicate = expressionToExpression(root, expression.left) - val rightPredicate = expressionToExpression(root, expression.right) + val leftPredicate = parseExpression(root, expression.left) + val rightPredicate = parseExpression(root, expression.right) when (expression.operator) { - BinaryLogicalOperator.AND -> criteriaBuilder.and(leftPredicate, rightPredicate) as Expression - BinaryLogicalOperator.OR -> criteriaBuilder.or(leftPredicate, rightPredicate) as Expression + BinaryLogicalOperator.AND -> criteriaBuilder.and(leftPredicate, rightPredicate) + BinaryLogicalOperator.OR -> criteriaBuilder.or(leftPredicate, rightPredicate) } } - is CriteriaExpression.Not -> criteriaBuilder.not(expressionToExpression(root, expression.expression)) as Expression + is CriteriaExpression.Not -> criteriaBuilder.not(parseExpression(root, expression.expression)) is CriteriaExpression.ColumnPredicateExpression -> { val column = root.get(getColumnName(expression.column)) - columnPredicateToPredicate(column, expression.predicate) as Expression + columnPredicateToPredicate(column, expression.predicate) } + else -> throw VaultQueryException("Unexpected expression: $expression") } } - private fun expressionToPredicate(root: Root, expression: CriteriaExpression): Predicate { - return expressionToExpression(root, expression) as Predicate + private fun parseAggregateFunction(root: Root, expression: CriteriaExpression.AggregateFunctionExpression): Expression? { + val column = root.get(getColumnName(expression.column)) + val columnPredicate = expression.predicate + when (columnPredicate) { + is ColumnPredicate.AggregateFunction -> { + column as Path? + val aggregateExpression = + when (columnPredicate.type) { + AggregateFunctionType.SUM -> criteriaBuilder.sum(column) + AggregateFunctionType.AVG -> criteriaBuilder.avg(column) + AggregateFunctionType.COUNT -> criteriaBuilder.count(column) + AggregateFunctionType.MAX -> criteriaBuilder.max(column) + AggregateFunctionType.MIN -> criteriaBuilder.min(column) + } + aggregateExpressions.add(aggregateExpression) + // optionally order by this aggregate function + expression.orderBy?.let { + val orderCriteria = + when (expression.orderBy!!) { + Sort.Direction.ASC -> criteriaBuilder.asc(aggregateExpression) + Sort.Direction.DESC -> criteriaBuilder.desc(aggregateExpression) + } + criteriaQuery.orderBy(orderCriteria) + } + // add optional group by clauses + expression.groupByColumns?.let { columns -> + val groupByExpressions = + columns.map { column -> + val path = root.get(getColumnName(column)) + aggregateExpressions.add(path) + path + } + criteriaQuery.groupBy(groupByExpressions) + } + return aggregateExpression + } + else -> throw VaultQueryException("Not expecting $columnPredicate") + } } override fun parseCriteria(criteria: QueryCriteria.FungibleAssetQueryCriteria) : Collection { @@ -254,7 +298,8 @@ class HibernateQueryCriteriaParser(val contractType: Class, val joinPredicate = criteriaBuilder.equal(vaultStates.get("stateRef"), entityRoot.get("stateRef")) joinPredicates.add(joinPredicate) - predicateSet.add(expressionToPredicate(entityRoot, criteria.expression)) + // resolve general criteria expressions + parseExpression(entityRoot, criteria.expression, predicateSet) } catch (e: Exception) { e.message?.let { message -> @@ -303,7 +348,11 @@ class HibernateQueryCriteriaParser(val contractType: Class, parse(sorting) } - val selections = listOf(vaultStates).plus(rootEntities.map { it.value }) + val selections = + if (aggregateExpressions.isEmpty()) + listOf(vaultStates).plus(rootEntities.map { it.value }) + else + aggregateExpressions criteriaQuery.multiselect(selections) val combinedPredicates = joinPredicates.plus(predicateSet) criteriaQuery.where(*combinedPredicates.toTypedArray()) diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt index 4d43bd702d..bb3c5d8e88 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt @@ -18,19 +18,20 @@ import net.corda.core.node.services.vault.Sort import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.deserialize import net.corda.core.serialization.storageKryo +import net.corda.core.utilities.debug import net.corda.core.utilities.loggerFor import net.corda.node.services.database.HibernateConfiguration import net.corda.node.services.vault.schemas.jpa.VaultSchemaV1 import org.jetbrains.exposed.sql.transactions.TransactionManager import rx.subjects.PublishSubject import java.lang.Exception +import java.util.* import javax.persistence.EntityManager import javax.persistence.Tuple class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration, val updatesPublisher: PublishSubject) : SingletonSerializeAsToken(), VaultQueryService { - companion object { val log = loggerFor() } @@ -80,17 +81,24 @@ class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration, val results = query.resultList val statesAndRefs: MutableList> = mutableListOf() val statesMeta: MutableList = mutableListOf() + val otherResults: MutableList = mutableListOf() results.asSequence() .forEach { it -> - val it = it[0] as VaultSchemaV1.VaultStates - val stateRef = StateRef(SecureHash.parse(it.stateRef!!.txId!!), it.stateRef!!.index!!) - val state = it.contractState.deserialize>(storageKryo()) - statesMeta.add(Vault.StateMetadata(stateRef, it.contractStateClassName, it.recordedTime, it.consumedTime, it.stateStatus, it.notaryName, it.notaryKey, it.lockId, it.lockUpdateTime)) - statesAndRefs.add(StateAndRef(state, stateRef)) + if (it[0] is VaultSchemaV1.VaultStates) { + val it = it[0] as VaultSchemaV1.VaultStates + val stateRef = StateRef(SecureHash.parse(it.stateRef!!.txId!!), it.stateRef!!.index!!) + val state = it.contractState.deserialize>(storageKryo()) + statesMeta.add(Vault.StateMetadata(stateRef, it.contractStateClassName, it.recordedTime, it.consumedTime, it.stateStatus, it.notaryName, it.notaryKey, it.lockId, it.lockUpdateTime)) + statesAndRefs.add(StateAndRef(state, stateRef)) + } + else { + log.debug { "OtherResults: ${Arrays.toString(it.toArray())}" } + otherResults.addAll(it.toArray().asList()) + } } - return Vault.Page(states = statesAndRefs, statesMetadata = statesMeta, pageable = paging, stateTypes = criteriaParser.stateTypes, totalStatesAvailable = totalStates) as Vault.Page + return Vault.Page(states = statesAndRefs, statesMetadata = statesMeta, pageable = paging, stateTypes = criteriaParser.stateTypes, totalStatesAvailable = totalStates, otherResults = otherResults) as Vault.Page } catch (e: Exception) { log.error(e.message) diff --git a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java index 3ec565aaa8..ca3326affd 100644 --- a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java +++ b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java @@ -6,7 +6,7 @@ import net.corda.contracts.DealState; import net.corda.contracts.asset.Cash; import net.corda.core.contracts.*; import net.corda.core.contracts.testing.DummyLinearContract; -import net.corda.core.crypto.SecureHash; +import net.corda.core.crypto.*; import net.corda.core.identity.AbstractParty; import net.corda.core.messaging.DataFeed; import net.corda.core.node.services.Vault; @@ -45,10 +45,11 @@ import java.util.stream.StreamSupport; import static net.corda.contracts.asset.CashKt.getDUMMY_CASH_ISSUER; import static net.corda.contracts.asset.CashKt.getDUMMY_CASH_ISSUER_KEY; +import static net.corda.testing.CoreTestUtils.getBOC; +import static net.corda.testing.CoreTestUtils.getBOC_KEY; +import static net.corda.testing.CoreTestUtils.getBOC_PUBKEY; import static net.corda.core.contracts.ContractsDSL.USD; -import static net.corda.core.node.services.vault.QueryCriteriaKt.and; -import static net.corda.core.node.services.vault.QueryCriteriaKt.or; -import static net.corda.core.node.services.vault.QueryCriteriaUtilsKt.getMAX_PAGE_SIZE; +import static net.corda.core.node.services.vault.QueryCriteriaUtils.getMAX_PAGE_SIZE; import static net.corda.node.utilities.DatabaseSupportKt.configureDatabase; import static net.corda.node.utilities.DatabaseSupportKt.transaction; import static net.corda.testing.CoreTestUtils.getMEGA_CORP; @@ -188,8 +189,8 @@ public class VaultQueryJavaTests { QueryCriteria linearCriteriaAll = new LinearStateQueryCriteria(null, linearIds); QueryCriteria dealCriteriaAll = new LinearStateQueryCriteria(null, null, dealIds); - QueryCriteria compositeCriteria1 = or(dealCriteriaAll, linearCriteriaAll); - QueryCriteria compositeCriteria2 = and(vaultCriteria, compositeCriteria1); + QueryCriteria compositeCriteria1 = dealCriteriaAll.or(linearCriteriaAll); + QueryCriteria compositeCriteria2 = vaultCriteria.and(compositeCriteria1); PageSpecification pageSpec = new PageSpecification(0, getMAX_PAGE_SIZE()); Sort.SortColumn sortByUid = new Sort.SortColumn(new SortAttribute.Standard(Sort.LinearStateAttribute.UUID), Sort.Direction.DESC); @@ -224,14 +225,14 @@ public class VaultQueryJavaTests { Field attributeCurrency = CashSchemaV1.PersistentCashState.class.getDeclaredField("currency"); Field attributeQuantity = CashSchemaV1.PersistentCashState.class.getDeclaredField("pennies"); - CriteriaExpression currencyIndex = Builder.INSTANCE.equal(attributeCurrency, "USD"); - CriteriaExpression quantityIndex = Builder.INSTANCE.greaterThanOrEqual(attributeQuantity, 10L); + CriteriaExpression currencyIndex = Builder.equal(attributeCurrency, "USD"); + CriteriaExpression quantityIndex = Builder.greaterThanOrEqual(attributeQuantity, 10L); QueryCriteria customCriteria2 = new VaultCustomQueryCriteria(quantityIndex); QueryCriteria customCriteria1 = new VaultCustomQueryCriteria(currencyIndex); - QueryCriteria criteria = QueryCriteriaKt.and(QueryCriteriaKt.and(generalCriteria, customCriteria1), customCriteria2); + QueryCriteria criteria = generalCriteria.and(customCriteria1).and(customCriteria2); Vault.Page results = vaultQuerySvc.queryBy(Cash.State.class, criteria); // DOCEND VaultJavaQueryExample3 @@ -297,8 +298,8 @@ public class VaultQueryJavaTests { List dealParty = Collections.singletonList(getMEGA_CORP()); QueryCriteria dealCriteria = new LinearStateQueryCriteria(dealParty, null, dealIds); QueryCriteria linearCriteria = new LinearStateQueryCriteria(dealParty, linearIds, null); - QueryCriteria dealOrLinearIdCriteria = or(dealCriteria, linearCriteria); - QueryCriteria compositeCriteria = and(dealOrLinearIdCriteria, vaultCriteria); + QueryCriteria dealOrLinearIdCriteria = dealCriteria.or(linearCriteria); + QueryCriteria compositeCriteria = dealOrLinearIdCriteria.and(vaultCriteria); PageSpecification pageSpec = new PageSpecification(0, getMAX_PAGE_SIZE()); Sort.SortColumn sortByUid = new Sort.SortColumn(new SortAttribute.Standard(Sort.LinearStateAttribute.UUID), Sort.Direction.DESC); @@ -374,4 +375,169 @@ public class VaultQueryJavaTests { return tx; }); } + + /** + * Aggregation Functions + */ + + @Test + public void aggregateFunctionsWithoutGroupClause() { + transaction(database, tx -> { + + Amount dollars100 = new Amount<>(100, Currency.getInstance("USD")); + Amount dollars200 = new Amount<>(200, Currency.getInstance("USD")); + Amount dollars300 = new Amount<>(300, Currency.getInstance("USD")); + Amount pounds = new Amount<>(400, Currency.getInstance("GBP")); + Amount swissfrancs = new Amount<>(500, Currency.getInstance("CHF")); + + VaultFiller.fillWithSomeTestCash(services, dollars100, TestConstants.getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); + VaultFiller.fillWithSomeTestCash(services, dollars200, TestConstants.getDUMMY_NOTARY(), 2, 2, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); + VaultFiller.fillWithSomeTestCash(services, dollars300, TestConstants.getDUMMY_NOTARY(), 3, 3, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); + VaultFiller.fillWithSomeTestCash(services, pounds, TestConstants.getDUMMY_NOTARY(), 4, 4, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); + VaultFiller.fillWithSomeTestCash(services, swissfrancs, TestConstants.getDUMMY_NOTARY(), 5, 5, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); + + try { + // DOCSTART VaultJavaQueryExample21 + Field pennies = CashSchemaV1.PersistentCashState.class.getDeclaredField("pennies"); + + QueryCriteria sumCriteria = new VaultCustomQueryCriteria(Builder.sum(pennies)); + QueryCriteria countCriteria = new VaultCustomQueryCriteria(Builder.count(pennies)); + QueryCriteria maxCriteria = new VaultCustomQueryCriteria(Builder.max(pennies)); + QueryCriteria minCriteria = new VaultCustomQueryCriteria(Builder.min(pennies)); + QueryCriteria avgCriteria = new VaultCustomQueryCriteria(Builder.avg(pennies)); + + QueryCriteria criteria = sumCriteria.and(countCriteria).and(maxCriteria).and(minCriteria).and(avgCriteria); + Vault.Page results = vaultQuerySvc.queryBy(Cash.State.class, criteria); + // DOCEND VaultJavaQueryExample21 + + assertThat(results.getOtherResults()).hasSize(5); + assertThat(results.getOtherResults().get(0)).isEqualTo(1500L); + assertThat(results.getOtherResults().get(1)).isEqualTo(15L); + assertThat(results.getOtherResults().get(2)).isEqualTo(113L); + assertThat(results.getOtherResults().get(3)).isEqualTo(87L); + assertThat(results.getOtherResults().get(4)).isEqualTo(100.0); + + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } + return tx; + }); + } + + @Test + public void aggregateFunctionsWithSingleGroupClause() { + transaction(database, tx -> { + + Amount dollars100 = new Amount<>(100, Currency.getInstance("USD")); + Amount dollars200 = new Amount<>(200, Currency.getInstance("USD")); + Amount dollars300 = new Amount<>(300, Currency.getInstance("USD")); + Amount pounds = new Amount<>(400, Currency.getInstance("GBP")); + Amount swissfrancs = new Amount<>(500, Currency.getInstance("CHF")); + + VaultFiller.fillWithSomeTestCash(services, dollars100, TestConstants.getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); + VaultFiller.fillWithSomeTestCash(services, dollars200, TestConstants.getDUMMY_NOTARY(), 2, 2, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); + VaultFiller.fillWithSomeTestCash(services, dollars300, TestConstants.getDUMMY_NOTARY(), 3, 3, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); + VaultFiller.fillWithSomeTestCash(services, pounds, TestConstants.getDUMMY_NOTARY(), 4, 4, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); + VaultFiller.fillWithSomeTestCash(services, swissfrancs, TestConstants.getDUMMY_NOTARY(), 5, 5, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); + + try { + // DOCSTART VaultJavaQueryExample22 + Field pennies = CashSchemaV1.PersistentCashState.class.getDeclaredField("pennies"); + Field currency = CashSchemaV1.PersistentCashState.class.getDeclaredField("currency"); + + QueryCriteria sumCriteria = new VaultCustomQueryCriteria(Builder.sum(pennies, Arrays.asList(currency))); + QueryCriteria countCriteria = new VaultCustomQueryCriteria(Builder.count(pennies)); + QueryCriteria maxCriteria = new VaultCustomQueryCriteria(Builder.max(pennies, Arrays.asList(currency))); + QueryCriteria minCriteria = new VaultCustomQueryCriteria(Builder.min(pennies, Arrays.asList(currency))); + QueryCriteria avgCriteria = new VaultCustomQueryCriteria(Builder.avg(pennies, Arrays.asList(currency))); + + QueryCriteria criteria = sumCriteria.and(countCriteria).and(maxCriteria).and(minCriteria).and(avgCriteria); + Vault.Page results = vaultQuerySvc.queryBy(Cash.State.class, criteria); + // DOCEND VaultJavaQueryExample22 + + assertThat(results.getOtherResults()).hasSize(27); + /** CHF */ + assertThat(results.getOtherResults().get(0)).isEqualTo(500L); + assertThat(results.getOtherResults().get(1)).isEqualTo("CHF"); + assertThat(results.getOtherResults().get(2)).isEqualTo(5L); + assertThat(results.getOtherResults().get(3)).isEqualTo(102L); + assertThat(results.getOtherResults().get(4)).isEqualTo("CHF"); + assertThat(results.getOtherResults().get(5)).isEqualTo(94L); + assertThat(results.getOtherResults().get(6)).isEqualTo("CHF"); + assertThat(results.getOtherResults().get(7)).isEqualTo(100.00); + assertThat(results.getOtherResults().get(8)).isEqualTo("CHF"); + /** GBP */ + assertThat(results.getOtherResults().get(9)).isEqualTo(400L); + assertThat(results.getOtherResults().get(10)).isEqualTo("GBP"); + assertThat(results.getOtherResults().get(11)).isEqualTo(4L); + assertThat(results.getOtherResults().get(12)).isEqualTo(103L); + assertThat(results.getOtherResults().get(13)).isEqualTo("GBP"); + assertThat(results.getOtherResults().get(14)).isEqualTo(93L); + assertThat(results.getOtherResults().get(15)).isEqualTo("GBP"); + assertThat(results.getOtherResults().get(16)).isEqualTo(100.0); + assertThat(results.getOtherResults().get(17)).isEqualTo("GBP"); + /** USD */ + assertThat(results.getOtherResults().get(18)).isEqualTo(600L); + assertThat(results.getOtherResults().get(19)).isEqualTo("USD"); + assertThat(results.getOtherResults().get(20)).isEqualTo(6L); + assertThat(results.getOtherResults().get(21)).isEqualTo(113L); + assertThat(results.getOtherResults().get(22)).isEqualTo("USD"); + assertThat(results.getOtherResults().get(23)).isEqualTo(87L); + assertThat(results.getOtherResults().get(24)).isEqualTo("USD"); + assertThat(results.getOtherResults().get(25)).isEqualTo(100.0); + assertThat(results.getOtherResults().get(26)).isEqualTo("USD"); + + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } + return tx; + }); + } + + @Test + public void aggregateFunctionsSumByIssuerAndCurrencyAndSortByAggregateSum() { + transaction(database, tx -> { + + Amount dollars100 = new Amount<>(100, Currency.getInstance("USD")); + Amount dollars200 = new Amount<>(200, Currency.getInstance("USD")); + Amount pounds300 = new Amount<>(300, Currency.getInstance("GBP")); + Amount pounds400 = new Amount<>(400, Currency.getInstance("GBP")); + + VaultFiller.fillWithSomeTestCash(services, dollars100, TestConstants.getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); + VaultFiller.fillWithSomeTestCash(services, dollars200, TestConstants.getDUMMY_NOTARY(), 2, 2, new Random(0L), new OpaqueBytes("1".getBytes()), null, getBOC().ref(new OpaqueBytes("1".getBytes())), getBOC_KEY()); + VaultFiller.fillWithSomeTestCash(services, pounds300, TestConstants.getDUMMY_NOTARY(), 3, 3, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); + VaultFiller.fillWithSomeTestCash(services, pounds400, TestConstants.getDUMMY_NOTARY(), 4, 4, new Random(0L), new OpaqueBytes("1".getBytes()), null, getBOC().ref(new OpaqueBytes("1".getBytes())), getBOC_KEY()); + + try { + // DOCSTART VaultJavaQueryExample23 + Field pennies = CashSchemaV1.PersistentCashState.class.getDeclaredField("pennies"); + Field currency = CashSchemaV1.PersistentCashState.class.getDeclaredField("currency"); + Field issuerParty = CashSchemaV1.PersistentCashState.class.getDeclaredField("issuerParty"); + + QueryCriteria sumCriteria = new VaultCustomQueryCriteria(Builder.sum(pennies, Arrays.asList(issuerParty, currency), Sort.Direction.DESC)); + + Vault.Page results = vaultQuerySvc.queryBy(Cash.State.class, sumCriteria); + // DOCEND VaultJavaQueryExample23 + + assertThat(results.getOtherResults()).hasSize(12); + + assertThat(results.getOtherResults().get(0)).isEqualTo(400L); + assertThat(results.getOtherResults().get(1)).isEqualTo(EncodingUtils.toBase58String(getBOC_PUBKEY())); + assertThat(results.getOtherResults().get(2)).isEqualTo("GBP"); + assertThat(results.getOtherResults().get(3)).isEqualTo(300L); + assertThat(results.getOtherResults().get(4)).isEqualTo(EncodingUtils.toBase58String(getDUMMY_CASH_ISSUER().getParty().getOwningKey())); + assertThat(results.getOtherResults().get(5)).isEqualTo("GBP"); + assertThat(results.getOtherResults().get(6)).isEqualTo(200L); + assertThat(results.getOtherResults().get(7)).isEqualTo(EncodingUtils.toBase58String(getBOC_PUBKEY())); + assertThat(results.getOtherResults().get(8)).isEqualTo("USD"); + assertThat(results.getOtherResults().get(9)).isEqualTo(100L); + assertThat(results.getOtherResults().get(10)).isEqualTo(EncodingUtils.toBase58String(getDUMMY_CASH_ISSUER().getParty().getOwningKey())); + assertThat(results.getOtherResults().get(11)).isEqualTo("USD"); + + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } + return tx; + }); + } } diff --git a/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt index 0514141451..b7f023d8af 100644 --- a/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt @@ -1,6 +1,7 @@ package net.corda.node.services.database import net.corda.contracts.asset.Cash +import net.corda.contracts.asset.DUMMY_CASH_ISSUER import net.corda.contracts.asset.DummyFungibleContract import net.corda.testing.contracts.consumeCash import net.corda.testing.contracts.fillWithSomeTestCash @@ -31,6 +32,8 @@ import net.corda.schemas.CashSchemaV1 import net.corda.schemas.SampleCashSchemaV2 import net.corda.schemas.SampleCashSchemaV3 import net.corda.testing.BOB_PUBKEY +import net.corda.testing.BOC +import net.corda.testing.BOC_KEY import net.corda.testing.node.MockServices import net.corda.testing.node.makeTestDataSourceProperties import org.assertj.core.api.Assertions @@ -301,6 +304,109 @@ class HibernateConfigurationTest { } } + @Test + fun `calculate cash balances`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L)) // +$100 = $200 + services.fillWithSomeTestCash(50.POUNDS, DUMMY_NOTARY, 5, 5, Random(0L)) // £50 = £50 + services.fillWithSomeTestCash(25.POUNDS, DUMMY_NOTARY, 5, 5, Random(0L)) // +£25 = £175 + services.fillWithSomeTestCash(500.SWISS_FRANCS, DUMMY_NOTARY, 10, 10, Random(0L)) // CHF500 = CHF500 + services.fillWithSomeTestCash(250.SWISS_FRANCS, DUMMY_NOTARY, 5, 5, Random(0L)) // +CHF250 = CHF750 + } + + // structure query + val criteriaQuery = criteriaBuilder.createQuery(Tuple::class.java) + val cashStates = criteriaQuery.from(CashSchemaV1.PersistentCashState::class.java) + + // aggregate function + criteriaQuery.multiselect(cashStates.get("currency"), + criteriaBuilder.sum(cashStates.get("pennies"))) + // group by + criteriaQuery.groupBy(cashStates.get("currency")) + + // execute query + val queryResults = entityManager.createQuery(criteriaQuery).resultList + + queryResults.forEach { tuple -> println("${tuple.get(0)} = ${tuple.get(1)}") } + + assertThat(queryResults[0].get(0)).isEqualTo("CHF") + assertThat(queryResults[0].get(1)).isEqualTo(75000L) + assertThat(queryResults[1].get(0)).isEqualTo("GBP") + assertThat(queryResults[1].get(1)).isEqualTo(7500L) + assertThat(queryResults[2].get(0)).isEqualTo("USD") + assertThat(queryResults[2].get(1)).isEqualTo(20000L) + } + + @Test + fun `calculate cash balance for single currency`() { + database.transaction { + services.fillWithSomeTestCash(50.POUNDS, DUMMY_NOTARY, 5, 5, Random(0L)) // £50 = £50 + services.fillWithSomeTestCash(25.POUNDS, DUMMY_NOTARY, 5, 5, Random(0L)) // +£25 = £175 + } + + // structure query + val criteriaQuery = criteriaBuilder.createQuery(Tuple::class.java) + val cashStates = criteriaQuery.from(CashSchemaV1.PersistentCashState::class.java) + + // aggregate function + criteriaQuery.multiselect(cashStates.get("currency"), + criteriaBuilder.sum(cashStates.get("pennies"))) + + // where + criteriaQuery.where(criteriaBuilder.equal(cashStates.get("currency"), "GBP")) + + // group by + criteriaQuery.groupBy(cashStates.get("currency")) + + // execute query + val queryResults = entityManager.createQuery(criteriaQuery).resultList + + queryResults.forEach { tuple -> println("${tuple.get(0)} = ${tuple.get(1)}") } + + assertThat(queryResults[0].get(0)).isEqualTo("GBP") + assertThat(queryResults[0].get(1)).isEqualTo(7500L) + } + + @Test + fun `calculate and order by cash balance for owner and currency`() { + database.transaction { + + services.fillWithSomeTestCash(200.DOLLARS, DUMMY_NOTARY, 2, 2, Random(0L), issuedBy = BOC.ref(1), issuerKey = BOC_KEY) + services.fillWithSomeTestCash(300.POUNDS, DUMMY_NOTARY, 3, 3, Random(0L), issuedBy = DUMMY_CASH_ISSUER) + services.fillWithSomeTestCash(400.POUNDS, DUMMY_NOTARY, 4, 4, Random(0L), issuedBy = BOC.ref(2), issuerKey = BOC_KEY) + } + + // structure query + val criteriaQuery = criteriaBuilder.createQuery(Tuple::class.java) + val cashStates = criteriaQuery.from(CashSchemaV1.PersistentCashState::class.java) + + // aggregate function + criteriaQuery.multiselect(cashStates.get("currency"), + criteriaBuilder.sum(cashStates.get("pennies"))) + + // group by + criteriaQuery.groupBy(cashStates.get("issuerParty"), cashStates.get("currency")) + + // order by + criteriaQuery.orderBy(criteriaBuilder.desc(criteriaBuilder.sum(cashStates.get("pennies")))) + + // execute query + val queryResults = entityManager.createQuery(criteriaQuery).resultList + + queryResults.forEach { tuple -> println("${tuple.get(0)} = ${tuple.get(1)}") } + + assertThat(queryResults).hasSize(4) + assertThat(queryResults[0].get(0)).isEqualTo("GBP") + assertThat(queryResults[0].get(1)).isEqualTo(40000L) + assertThat(queryResults[1].get(0)).isEqualTo("GBP") + assertThat(queryResults[1].get(1)).isEqualTo(30000L) + assertThat(queryResults[2].get(0)).isEqualTo("USD") + assertThat(queryResults[2].get(1)).isEqualTo(20000L) + assertThat(queryResults[3].get(0)).isEqualTo("USD") + assertThat(queryResults[3].get(1)).isEqualTo(10000L) + } + /** * CashSchemaV2 = optimised Cash schema (extending FungibleState) */ diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index 88c2467e35..ecdb09843b 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -3,12 +3,12 @@ package net.corda.node.services.vault import net.corda.contracts.CommercialPaper import net.corda.contracts.Commodity import net.corda.contracts.DealState -import net.corda.contracts.DummyDealContract import net.corda.contracts.asset.Cash import net.corda.contracts.asset.DUMMY_CASH_ISSUER import net.corda.core.contracts.* import net.corda.core.contracts.testing.DummyLinearContract import net.corda.core.crypto.entropyToKeyPair +import net.corda.core.crypto.toBase58String import net.corda.core.days import net.corda.core.identity.Party import net.corda.core.node.services.* @@ -18,9 +18,6 @@ import net.corda.core.schemas.testing.DummyLinearStateSchemaV1 import net.corda.core.seconds import net.corda.core.serialization.OpaqueBytes import net.corda.core.transactions.SignedTransaction -import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.DUMMY_NOTARY_KEY -import net.corda.testing.TEST_TX_TIME import net.corda.node.services.database.HibernateConfiguration import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.vault.schemas.jpa.VaultSchemaV1 @@ -556,6 +553,143 @@ class VaultQueryTests { } } + @Test + fun `aggregate functions without group clause`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(200.DOLLARS, DUMMY_NOTARY, 2, 2, Random(0L)) + services.fillWithSomeTestCash(300.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) + services.fillWithSomeTestCash(400.POUNDS, DUMMY_NOTARY, 4, 4, Random(0L)) + services.fillWithSomeTestCash(500.SWISS_FRANCS, DUMMY_NOTARY, 5, 5, Random(0L)) + + // DOCSTART VaultQueryExample21 + val sum = builder { CashSchemaV1.PersistentCashState::pennies.sum() } + val sumCriteria = VaultCustomQueryCriteria(sum) + + val count = builder { CashSchemaV1.PersistentCashState::pennies.count() } + val countCriteria = VaultCustomQueryCriteria(count) + + val max = builder { CashSchemaV1.PersistentCashState::pennies.max() } + val maxCriteria = VaultCustomQueryCriteria(max) + + val min = builder { CashSchemaV1.PersistentCashState::pennies.min() } + val minCriteria = VaultCustomQueryCriteria(min) + + val avg = builder { CashSchemaV1.PersistentCashState::pennies.avg() } + val avgCriteria = VaultCustomQueryCriteria(avg) + + val results = vaultQuerySvc.queryBy>(sumCriteria + .and(countCriteria) + .and(maxCriteria) + .and(minCriteria) + .and(avgCriteria)) + // DOCEND VaultQueryExample21 + + assertThat(results.otherResults).hasSize(5) + assertThat(results.otherResults[0]).isEqualTo(150000L) + assertThat(results.otherResults[1]).isEqualTo(15L) + assertThat(results.otherResults[2]).isEqualTo(11298L) + assertThat(results.otherResults[3]).isEqualTo(8702L) + assertThat(results.otherResults[4]).isEqualTo(10000.0) + } + } + + @Test + fun `aggregate functions with single group clause`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(200.DOLLARS, DUMMY_NOTARY, 2, 2, Random(0L)) + services.fillWithSomeTestCash(300.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) + services.fillWithSomeTestCash(400.POUNDS, DUMMY_NOTARY, 4, 4, Random(0L)) + services.fillWithSomeTestCash(500.SWISS_FRANCS, DUMMY_NOTARY, 5, 5, Random(0L)) + + // DOCSTART VaultQueryExample22 + val sum = builder { CashSchemaV1.PersistentCashState::pennies.sum(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) } + val sumCriteria = VaultCustomQueryCriteria(sum) + + val max = builder { CashSchemaV1.PersistentCashState::pennies.max(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) } + val maxCriteria = VaultCustomQueryCriteria(max) + + val min = builder { CashSchemaV1.PersistentCashState::pennies.min(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) } + val minCriteria = VaultCustomQueryCriteria(min) + + val avg = builder { CashSchemaV1.PersistentCashState::pennies.avg(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) } + val avgCriteria = VaultCustomQueryCriteria(avg) + + val results = vaultQuerySvc.queryBy>(sumCriteria + .and(maxCriteria) + .and(minCriteria) + .and(avgCriteria)) + // DOCEND VaultQueryExample22 + + assertThat(results.otherResults).hasSize(24) + /** CHF */ + assertThat(results.otherResults[0]).isEqualTo(50000L) + assertThat(results.otherResults[1]).isEqualTo("CHF") + assertThat(results.otherResults[2]).isEqualTo(10274L) + assertThat(results.otherResults[3]).isEqualTo("CHF") + assertThat(results.otherResults[4]).isEqualTo(9481L) + assertThat(results.otherResults[5]).isEqualTo("CHF") + assertThat(results.otherResults[6]).isEqualTo(10000.0) + assertThat(results.otherResults[7]).isEqualTo("CHF") + /** GBP */ + assertThat(results.otherResults[8]).isEqualTo(40000L) + assertThat(results.otherResults[9]).isEqualTo("GBP") + assertThat(results.otherResults[10]).isEqualTo(10343L) + assertThat(results.otherResults[11]).isEqualTo("GBP") + assertThat(results.otherResults[12]).isEqualTo(9351L) + assertThat(results.otherResults[13]).isEqualTo("GBP") + assertThat(results.otherResults[14]).isEqualTo(10000.0) + assertThat(results.otherResults[15]).isEqualTo("GBP") + /** USD */ + assertThat(results.otherResults[16]).isEqualTo(60000L) + assertThat(results.otherResults[17]).isEqualTo("USD") + assertThat(results.otherResults[18]).isEqualTo(11298L) + assertThat(results.otherResults[19]).isEqualTo("USD") + assertThat(results.otherResults[20]).isEqualTo(8702L) + assertThat(results.otherResults[21]).isEqualTo("USD") + assertThat(results.otherResults[22]).isEqualTo(10000.0) + assertThat(results.otherResults[23]).isEqualTo("USD") + } + } + + @Test + fun `aggregate functions sum by issuer and currency and sort by aggregate sum`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = DUMMY_CASH_ISSUER) + services.fillWithSomeTestCash(200.DOLLARS, DUMMY_NOTARY, 2, 2, Random(0L), issuedBy = BOC.ref(1), issuerKey = BOC_KEY) + services.fillWithSomeTestCash(300.POUNDS, DUMMY_NOTARY, 3, 3, Random(0L), issuedBy = DUMMY_CASH_ISSUER) + services.fillWithSomeTestCash(400.POUNDS, DUMMY_NOTARY, 4, 4, Random(0L), issuedBy = BOC.ref(2), issuerKey = BOC_KEY) + + // DOCSTART VaultQueryExample23 + val sum = builder { CashSchemaV1.PersistentCashState::pennies.sum(groupByColumns = listOf(CashSchemaV1.PersistentCashState::issuerParty, + CashSchemaV1.PersistentCashState::currency), + orderBy = Sort.Direction.DESC) + } + + val results = vaultQuerySvc.queryBy>(VaultCustomQueryCriteria(sum)) + // DOCEND VaultQueryExample23 + + assertThat(results.otherResults).hasSize(12) + + assertThat(results.otherResults[0]).isEqualTo(40000L) + assertThat(results.otherResults[1]).isEqualTo(BOC_PUBKEY.toBase58String()) + assertThat(results.otherResults[2]).isEqualTo("GBP") + assertThat(results.otherResults[3]).isEqualTo(30000L) + assertThat(results.otherResults[4]).isEqualTo(DUMMY_CASH_ISSUER.party.owningKey.toBase58String()) + assertThat(results.otherResults[5]).isEqualTo("GBP") + assertThat(results.otherResults[6]).isEqualTo(20000L) + assertThat(results.otherResults[7]).isEqualTo(BOC_PUBKEY.toBase58String()) + assertThat(results.otherResults[8]).isEqualTo("USD") + assertThat(results.otherResults[9]).isEqualTo(10000L) + assertThat(results.otherResults[10]).isEqualTo(DUMMY_CASH_ISSUER.party.owningKey.toBase58String()) + assertThat(results.otherResults[11]).isEqualTo("USD") + } + } + private val TODAY = LocalDate.now().atStartOfDay().toInstant(ZoneOffset.UTC) @Test @@ -862,7 +996,7 @@ class VaultQueryTests { // DOCSTART VaultQueryExample9 val linearStateCriteria = LinearStateQueryCriteria(linearId = listOf(linearId), status = Vault.StateStatus.ALL) val vaultCriteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) - val results = vaultQuerySvc.queryBy(linearStateCriteria.and(vaultCriteria)) + val results = vaultQuerySvc.queryBy(linearStateCriteria and vaultCriteria) // DOCEND VaultQueryExample9 assertThat(results.states).hasSize(4) } @@ -1176,6 +1310,52 @@ class VaultQueryTests { } } + @Test + fun `unconsumed cash balance for single currency`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(200.DOLLARS, DUMMY_NOTARY, 2, 2, Random(0L)) + + val sum = builder { CashSchemaV1.PersistentCashState::pennies.sum(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) } + val sumCriteria = VaultCustomQueryCriteria(sum) + + val ccyIndex = builder { CashSchemaV1.PersistentCashState::currency.equal(USD.currencyCode) } + val ccyCriteria = VaultCustomQueryCriteria(ccyIndex) + + val results = vaultQuerySvc.queryBy>(sumCriteria.and(ccyCriteria)) + + assertThat(results.otherResults).hasSize(2) + assertThat(results.otherResults[0]).isEqualTo(30000L) + assertThat(results.otherResults[1]).isEqualTo("USD") + } + } + + @Test + fun `unconsumed cash balances for all currencies`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(200.DOLLARS, DUMMY_NOTARY, 2, 2, Random(0L)) + services.fillWithSomeTestCash(300.POUNDS, DUMMY_NOTARY, 3, 3, Random(0L)) + services.fillWithSomeTestCash(400.POUNDS, DUMMY_NOTARY, 4, 4, Random(0L)) + services.fillWithSomeTestCash(500.SWISS_FRANCS, DUMMY_NOTARY, 5, 5, Random(0L)) + services.fillWithSomeTestCash(600.SWISS_FRANCS, DUMMY_NOTARY, 6, 6, Random(0L)) + + val ccyIndex = builder { CashSchemaV1.PersistentCashState::pennies.sum(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) } + val criteria = VaultCustomQueryCriteria(ccyIndex) + val results = vaultQuerySvc.queryBy>(criteria) + + assertThat(results.otherResults).hasSize(6) + assertThat(results.otherResults[0]).isEqualTo(110000L) + assertThat(results.otherResults[1]).isEqualTo("CHF") + assertThat(results.otherResults[2]).isEqualTo(70000L) + assertThat(results.otherResults[3]).isEqualTo("GBP") + assertThat(results.otherResults[4]).isEqualTo(30000L) + assertThat(results.otherResults[5]).isEqualTo("USD") + } + } + @Test fun `unconsumed fungible assets for quantity greater than`() { database.transaction { From 182c9cceb540585f9514fe8f34fe8883677bad82 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Thu, 6 Jul 2017 10:08:17 +0100 Subject: [PATCH 57/97] Cleaned up the QueryCriteria API to be more Java friendly --- .../corda/core/node/services/vault/QueryCriteria.kt | 6 +++--- .../core/node/services/vault/QueryCriteriaUtils.kt | 7 ++++--- docs/source/changelog.rst | 3 +++ .../net/corda/contracts/JavaCommercialPaper.java | 8 ++++---- .../node/services/vault/VaultQueryJavaTests.java | 13 +++++++------ 5 files changed, 21 insertions(+), 16 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt index 4726ef8199..fc3f0cad0d 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt @@ -96,13 +96,13 @@ sealed class QueryCriteria { } // enable composition of [QueryCriteria] - data class AndComposition(val a: QueryCriteria, val b: QueryCriteria): QueryCriteria() { + private data class AndComposition(val a: QueryCriteria, val b: QueryCriteria): QueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { return parser.parseAnd(this.a, this.b) } } - data class OrComposition(val a: QueryCriteria, val b: QueryCriteria): QueryCriteria() { + private data class OrComposition(val a: QueryCriteria, val b: QueryCriteria): QueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { return parser.parseOr(this.a, this.b) } @@ -128,4 +128,4 @@ interface IQueryCriteriaParser { fun parseOr(left: QueryCriteria, right: QueryCriteria): Collection fun parseAnd(left: QueryCriteria, right: QueryCriteria): Collection fun parse(criteria: QueryCriteria, sorting: Sort? = null) : Collection -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt index 69c5e6ce8d..8f3a528b77 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt @@ -91,6 +91,7 @@ fun resolveEnclosingObjectFromExpression(expression: CriteriaExpression resolveEnclosingObjectFromColumn(column: Column): Class { return when (column) { is Column.Java -> column.field.declaringClass as Class @@ -118,14 +119,14 @@ fun getColumnName(column: Column): String { * paging and sorting capability: * https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/repository/PagingAndSortingRepository.html */ -val DEFAULT_PAGE_NUM = 0 -val DEFAULT_PAGE_SIZE = 200 +const val DEFAULT_PAGE_NUM = 0 +const val DEFAULT_PAGE_SIZE = 200 /** * Note: this maximum size will be configurable in future (to allow for large JVM heap sized node configurations) * Use [PageSpecification] to correctly handle a number of bounded pages of [MAX_PAGE_SIZE]. */ -val MAX_PAGE_SIZE = 512 +const val MAX_PAGE_SIZE = 512 /** * PageSpecification allows specification of a page number (starting from 0 as default) and page size (defaulting to diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index e95a2ebc56..b0a46f0cab 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -20,6 +20,9 @@ UNRELEASED * Mock identity constants used in tests, such as ``ALICE``, ``BOB``, ``DUMMY_NOTARY``, have moved to ``net.corda.testing`` in the ``test-utils`` module. +* In Java, ``QueryCriteriaUtilsKt`` has moved to ``QueryCriteriaUtils``. Also ``and`` and ``or`` are now instance methods + of ``QueryCrtieria``. + Milestone 13 ------------ diff --git a/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java b/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java index 07b071c98e..df6b29e5be 100644 --- a/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java +++ b/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java @@ -2,6 +2,7 @@ package net.corda.contracts; import co.paralleluniverse.fibers.Suspendable; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; import kotlin.Pair; import kotlin.Unit; import net.corda.contracts.asset.CashKt; @@ -28,7 +29,6 @@ import java.util.List; import java.util.Set; import java.util.stream.Collectors; -import static kotlin.collections.CollectionsKt.single; import static net.corda.core.contracts.ContractsDSL.requireSingleCommand; import static net.corda.core.contracts.ContractsDSL.requireThat; @@ -175,7 +175,7 @@ public class JavaCommercialPaper implements Contract { State groupingKey) { AuthenticatedObject cmd = requireSingleCommand(tx.getCommands(), Commands.Move.class); // There should be only a single input due to aggregation above - State input = single(inputs); + State input = Iterables.getOnlyElement(inputs); if (!cmd.getSigners().contains(input.getOwner().getOwningKey())) throw new IllegalStateException("Failed requirement: the transaction is signed by the owner of the CP"); @@ -208,7 +208,7 @@ public class JavaCommercialPaper implements Contract { AuthenticatedObject cmd = requireSingleCommand(tx.getCommands(), Commands.Redeem.class); // There should be only a single input due to aggregation above - State input = single(inputs); + State input = Iterables.getOnlyElement(inputs); if (!cmd.getSigners().contains(input.getOwner().getOwningKey())) throw new IllegalStateException("Failed requirement: the transaction is signed by the owner of the CP"); @@ -249,7 +249,7 @@ public class JavaCommercialPaper implements Contract { @NotNull List> commands, State groupingKey) { AuthenticatedObject cmd = requireSingleCommand(tx.getCommands(), Commands.Issue.class); - State output = single(outputs); + State output = Iterables.getOnlyElement(outputs); TimeWindow timeWindowCommand = tx.getTimeWindow(); Instant time = null == timeWindowCommand ? null diff --git a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java index ca3326affd..8e68e28aa6 100644 --- a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java +++ b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java @@ -49,7 +49,7 @@ import static net.corda.testing.CoreTestUtils.getBOC; import static net.corda.testing.CoreTestUtils.getBOC_KEY; import static net.corda.testing.CoreTestUtils.getBOC_PUBKEY; import static net.corda.core.contracts.ContractsDSL.USD; -import static net.corda.core.node.services.vault.QueryCriteriaUtils.getMAX_PAGE_SIZE; +import static net.corda.core.node.services.vault.QueryCriteriaUtils.MAX_PAGE_SIZE; import static net.corda.node.utilities.DatabaseSupportKt.configureDatabase; import static net.corda.node.utilities.DatabaseSupportKt.transaction; import static net.corda.testing.CoreTestUtils.getMEGA_CORP; @@ -60,7 +60,7 @@ import static org.assertj.core.api.Assertions.assertThat; public class VaultQueryJavaTests { private MockServices services; - VaultService vaultSvc; + private VaultService vaultSvc; private VaultQueryService vaultQuerySvc; private Closeable dataSource; private Database database; @@ -82,6 +82,7 @@ public class VaultQueryJavaTests { return makeVaultService(dataSourceProps, hibernateConfig); } + @NotNull @Override public VaultQueryService getVaultQueryService() { return new HibernateVaultQueryImpl(hibernateConfig, getVaultService().getUpdatesPublisher()); @@ -192,7 +193,7 @@ public class VaultQueryJavaTests { QueryCriteria compositeCriteria1 = dealCriteriaAll.or(linearCriteriaAll); QueryCriteria compositeCriteria2 = vaultCriteria.and(compositeCriteria1); - PageSpecification pageSpec = new PageSpecification(0, getMAX_PAGE_SIZE()); + PageSpecification pageSpec = new PageSpecification(0, MAX_PAGE_SIZE); Sort.SortColumn sortByUid = new Sort.SortColumn(new SortAttribute.Standard(Sort.LinearStateAttribute.UUID), Sort.Direction.DESC); Sort sorting = new Sort(ImmutableSet.of(sortByUid)); Vault.Page results = vaultQuerySvc.queryBy(LinearState.class, compositeCriteria2, pageSpec, sorting); @@ -269,8 +270,8 @@ public class VaultQueryJavaTests { VaultQueryCriteria criteria = new VaultQueryCriteria(Vault.StateStatus.UNCONSUMED, contractStateTypes); DataFeed, Vault.Update> results = vaultQuerySvc.trackBy(ContractState.class, criteria); - Vault.Page snapshot = results.getCurrent(); - Observable updates = results.getFuture(); + Vault.Page snapshot = results.getSnapshot(); + Observable updates = results.getUpdates(); // DOCEND VaultJavaQueryExample4 assertThat(snapshot.getStates()).hasSize(3); @@ -301,7 +302,7 @@ public class VaultQueryJavaTests { QueryCriteria dealOrLinearIdCriteria = dealCriteria.or(linearCriteria); QueryCriteria compositeCriteria = dealOrLinearIdCriteria.and(vaultCriteria); - PageSpecification pageSpec = new PageSpecification(0, getMAX_PAGE_SIZE()); + PageSpecification pageSpec = new PageSpecification(0, MAX_PAGE_SIZE); Sort.SortColumn sortByUid = new Sort.SortColumn(new SortAttribute.Standard(Sort.LinearStateAttribute.UUID), Sort.Direction.DESC); Sort sorting = new Sort(ImmutableSet.of(sortByUid)); DataFeed, Vault.Update> results = vaultQuerySvc.trackBy(ContractState.class, compositeCriteria, pageSpec, sorting); From 177f591e57451f449c5e69f31ef0fba83cbe4489 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Thu, 6 Jul 2017 12:25:54 +0100 Subject: [PATCH 58/97] Uses brute-force approach to add a dropdown. --- .../_templates/layout_for_doc_website.html | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/source/_templates/layout_for_doc_website.html b/docs/source/_templates/layout_for_doc_website.html index 2ebb4d0cb6..841ca935b3 100644 --- a/docs/source/_templates/layout_for_doc_website.html +++ b/docs/source/_templates/layout_for_doc_website.html @@ -10,6 +10,32 @@ API reference: Kotlin/ Slack
+ +
{% endblock %} {% block footer %} From 54aa4802f986189c2c11a9516fe3e02f54d0b0ca Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Thu, 6 Jul 2017 12:27:38 +0100 Subject: [PATCH 59/97] Clarifying the need for a single Party c'tor for InitiatedBy flows --- core/src/main/kotlin/net/corda/core/flows/InitiatedBy.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/flows/InitiatedBy.kt b/core/src/main/kotlin/net/corda/core/flows/InitiatedBy.kt index 25f2433ea0..a5f9c709a2 100644 --- a/core/src/main/kotlin/net/corda/core/flows/InitiatedBy.kt +++ b/core/src/main/kotlin/net/corda/core/flows/InitiatedBy.kt @@ -4,8 +4,10 @@ import kotlin.annotation.AnnotationTarget.CLASS import kotlin.reflect.KClass /** - * This annotation is required by any [FlowLogic] that is designed to be initiated by a counterparty flow. The flow that - * does the initiating is specified by the [value] property and itself must be annotated with [InitiatingFlow]. + * This annotation is required by any [FlowLogic] that is designed to be initiated by a counterparty flow. The class must + * have at least a constructor which takes in a single [net.corda.core.identity.Party] parameter which represents the + * initiating counterparty. The [FlowLogic] that does the initiating is specified by the [value] property and itself must be annotated + * with [InitiatingFlow]. * * The node on startup scans for [FlowLogic]s which are annotated with this and automatically registers the initiating * to initiated flow mapping. From c6e165947b524e895200c19d355be380511022be Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Tue, 4 Jul 2017 12:12:50 +0100 Subject: [PATCH 60/97] Added background checkpoint checker to make sure they're at least deserialisable --- .../core/serialization/CordaClassResolver.kt | 19 +++++- .../serialization/DefaultKryoCustomizer.kt | 20 ++++-- .../net/corda/core/serialization/Kryo.kt | 1 + .../corda/core/utilities/UntrustworthyData.kt | 8 +-- .../corda/flows/TransactionKeyFlowTests.kt | 6 ++ docs/source/corda-configuration-file.rst | 8 ++- .../statemachine/StateMachineManager.kt | 63 +++++++++++++------ .../net/corda/node/CordaRPCOpsImplTest.kt | 6 ++ .../corda/node/messaging/AttachmentTests.kt | 8 ++- .../node/messaging/InMemoryMessagingTests.kt | 8 ++- .../node/messaging/TwoPartyTradeFlowTests.kt | 6 +- .../node/services/MockServiceHubInternal.kt | 3 +- .../corda/node/services/NotaryChangeTests.kt | 8 ++- .../events/NodeSchedulerServiceTest.kt | 11 +++- .../persistence/DataVendingServiceTests.kt | 8 ++- .../statemachine/FlowFrameworkTests.kt | 13 ++-- .../transactions/NotaryServiceTests.kt | 26 +++++--- .../ValidatingNotaryServiceTests.kt | 17 +++-- .../corda/irs/api/NodeInterestRatesTest.kt | 7 +-- .../kotlin/net/corda/testing/node/MockNode.kt | 2 - 20 files changed, 181 insertions(+), 67 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/serialization/CordaClassResolver.kt b/core/src/main/kotlin/net/corda/core/serialization/CordaClassResolver.kt index c97087025a..11f450cac9 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/CordaClassResolver.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/CordaClassResolver.kt @@ -1,6 +1,8 @@ package net.corda.core.serialization import com.esotericsoftware.kryo.* +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output import com.esotericsoftware.kryo.util.DefaultClassResolver import com.esotericsoftware.kryo.util.Util import net.corda.core.node.AttachmentsClassLoader @@ -77,11 +79,18 @@ class CordaClassResolver(val whitelist: ClassWhitelist, val amqpEnabled: Boolean // case for flow checkpoints (ignoring all cases where AMQP is disabled) since our top level messaging data structures // are annotated and once we enter AMQP serialisation we stay with it for the entire object subgraph. if (!hasAnnotation || !amqpEnabled) { + val objectInstance = try { + type.kotlin.objectInstance + } catch (t: Throwable) { + // objectInstance will throw if the type is something like a lambda + null + } // We have to set reference to true, since the flag influences how String fields are treated and we want it to be consistent. val references = kryo.references try { kryo.references = true - return register(Registration(type, kryo.getDefaultSerializer(type), NAME.toInt())) + val serializer = if (objectInstance != null) KotlinObjectSerializer(objectInstance) else kryo.getDefaultSerializer(type) + return register(Registration(type, serializer, NAME.toInt())) } finally { kryo.references = references } @@ -91,6 +100,12 @@ class CordaClassResolver(val whitelist: ClassWhitelist, val amqpEnabled: Boolean } } + // Trivial Serializer which simply returns the given instance which we already know is a Kotlin object + private class KotlinObjectSerializer(val objectInstance: Any) : Serializer() { + override fun read(kryo: Kryo, input: Input, type: Class): Any = objectInstance + override fun write(kryo: Kryo, output: Output, obj: Any) = Unit + } + // We don't allow the annotation for classes in attachments for now. The class will be on the main classpath if we have the CorDapp installed. // We also do not allow extension of KryoSerializable for annotated classes, or combination with @DefaultSerializer for custom serialisation. // TODO: Later we can support annotations on attachment classes and spin up a proxy via bytecode that we know is harmless. @@ -165,8 +180,6 @@ class GlobalTransientClassWhiteList(val delegate: ClassWhitelist) : MutableClass /** * This class is not currently used, but can be installed to log a large number of missing entries from the whitelist * and was used to track down the initial set. - * - * @suppress */ @Suppress("unused") class LoggingWhitelist(val delegate: ClassWhitelist, val global: Boolean = true) : MutableClassWhitelist { diff --git a/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt b/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt index 3f903409fa..edf3792bea 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt @@ -25,14 +25,16 @@ import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPrivateCrtKey import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPublicKey import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PrivateKey import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PublicKey +import org.objenesis.instantiator.ObjectInstantiator +import org.objenesis.strategy.InstantiatorStrategy import org.objenesis.strategy.StdInstantiatorStrategy import org.slf4j.Logger import sun.security.provider.certpath.X509CertPath import java.io.BufferedInputStream import java.io.FileInputStream import java.io.InputStream +import java.lang.reflect.Modifier.isPublic import java.security.cert.CertPath -import java.security.cert.X509Certificate import java.util.* object DefaultKryoCustomizer { @@ -51,9 +53,7 @@ object DefaultKryoCustomizer { // Take the safest route here and allow subclasses to have fields named the same as super classes. fieldSerializerConfig.cachedFieldNameStrategy = FieldSerializer.CachedFieldNameStrategy.EXTENDED - // Allow construction of objects using a JVM backdoor that skips invoking the constructors, if there is no - // no-arg constructor available. - instantiatorStrategy = Kryo.DefaultInstantiatorStrategy(StdInstantiatorStrategy()) + instantiatorStrategy = CustomInstantiatorStrategy() register(Arrays.asList("").javaClass, ArraysAsListSerializer()) register(SignedTransaction::class.java, ImmutableClassSerializer(SignedTransaction::class)) @@ -119,4 +119,16 @@ object DefaultKryoCustomizer { pluginRegistries.forEach { it.customizeSerialization(customization) } } } + + private class CustomInstantiatorStrategy : InstantiatorStrategy { + private val fallbackStrategy = StdInstantiatorStrategy() + // Use this to allow construction of objects using a JVM backdoor that skips invoking the constructors, if there + // is no no-arg constructor available. + private val defaultStrategy = Kryo.DefaultInstantiatorStrategy(fallbackStrategy) + override fun newInstantiatorOf(type: Class): ObjectInstantiator { + // However this doesn't work for non-public classes in the java. namespace + val strat = if (type.name.startsWith("java.") && !isPublic(type.modifiers)) fallbackStrategy else defaultStrategy + return strat.newInstantiatorOf(type) + } + } } diff --git a/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt b/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt index 4d3ab0fe5a..de0dea59cb 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt @@ -463,6 +463,7 @@ inline fun readListOfLength(kryo: Kryo, input: Input, minLen: Int = } /** Marker interface for kotlin object definitions so that they are deserialized as the singleton instance. */ +// TODO This is not needed anymore interface DeserializeAsKotlinObjectDef /** Serializer to deserialize kotlin object definitions marked with [DeserializeAsKotlinObjectDef]. */ diff --git a/core/src/main/kotlin/net/corda/core/utilities/UntrustworthyData.kt b/core/src/main/kotlin/net/corda/core/utilities/UntrustworthyData.kt index 48ecc3da4e..afa519fcec 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/UntrustworthyData.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/UntrustworthyData.kt @@ -2,6 +2,7 @@ package net.corda.core.utilities import co.paralleluniverse.fibers.Suspendable import net.corda.core.flows.FlowException +import java.io.Serializable /** * A small utility to approximate taint tracking: if a method gives you back one of these, it means the data came from @@ -23,11 +24,8 @@ class UntrustworthyData(private val fromUntrustedWorld: T) { @Throws(FlowException::class) fun unwrap(validator: Validator) = validator.validate(fromUntrustedWorld) - @Suppress("DEPRECATION") - @Deprecated("This old name was confusing, use unwrap instead", replaceWith = ReplaceWith("unwrap")) - inline fun validate(validator: (T) -> R) = validator(data) - - interface Validator { + @FunctionalInterface + interface Validator : Serializable { @Suspendable @Throws(FlowException::class) fun validate(data: T): R diff --git a/core/src/test/kotlin/net/corda/flows/TransactionKeyFlowTests.kt b/core/src/test/kotlin/net/corda/flows/TransactionKeyFlowTests.kt index 81480b842b..486e24b3e8 100644 --- a/core/src/test/kotlin/net/corda/flows/TransactionKeyFlowTests.kt +++ b/core/src/test/kotlin/net/corda/flows/TransactionKeyFlowTests.kt @@ -7,6 +7,7 @@ import net.corda.testing.ALICE import net.corda.testing.BOB import net.corda.testing.DUMMY_NOTARY import net.corda.testing.node.MockNetwork +import org.junit.After import org.junit.Before import org.junit.Test import kotlin.test.assertEquals @@ -22,6 +23,11 @@ class TransactionKeyFlowTests { mockNet = MockNetwork(false) } + @After + fun cleanUp() { + mockNet.stopNodes() + } + @Test fun `issue key`() { // We run this in parallel threads to help catch any race conditions that may exist. diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index 8e04b7ac06..0bf02b3467 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -128,11 +128,13 @@ path to the node's base directory. :password: The password :permissions: A list of permission strings which RPC methods can use to control access - If this field is absent or an empty list then RPC is effectively locked down. Alternatively, if it contains the string ``ALL`` then the user is permitted to use *any* RPC method. This value is intended for administrator users and for developers. + If this field is absent or an empty list then RPC is effectively locked down. Alternatively, if it contains the string + ``ALL`` then the user is permitted to use *any* RPC method. This value is intended for administrator users and for developers. -:devMode: This flag indicate if the node is running in development mode. On startup, if the keystore ``/certificates/sslkeystore.jks`` +:devMode: This flag sets the node to run in development mode. On startup, if the keystore ``/certificates/sslkeystore.jks`` does not exist, a developer keystore will be used if ``devMode`` is true. The node will exit if ``devMode`` is false - and keystore does not exist. + and the keystore does not exist. ``devMode`` also turns on background checking of flow checkpoints to shake out any + bugs in the checkpointing process. :detectPublicIp: This flag toggles the auto IP detection behaviour, it is enabled by default. On startup the node will attempt to discover its externally visible IP address first by looking for any public addresses on its network diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt index 1e274ebf9a..8066a7293f 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt @@ -13,6 +13,7 @@ import com.esotericsoftware.kryo.io.Output import com.esotericsoftware.kryo.pool.KryoPool import com.google.common.collect.HashMultimap import com.google.common.util.concurrent.ListenableFuture +import com.google.common.util.concurrent.MoreExecutors import io.requery.util.CloseableIterator import net.corda.core.* import net.corda.core.crypto.SecureHash @@ -35,15 +36,18 @@ import net.corda.node.services.messaging.TopicSession import net.corda.node.utilities.* import org.apache.activemq.artemis.utils.ReusableLatch import org.jetbrains.exposed.sql.Database +import org.slf4j.Logger import rx.Observable import rx.subjects.PublishSubject import java.util.* import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit.SECONDS import javax.annotation.concurrent.ThreadSafe import kotlin.collections.ArrayList /** - * A StateMachineManager is responsible for coordination and persistence of multiple [FlowStateMachine] objects. + * A StateMachineManager is responsible for coordination and persistence of multiple [FlowStateMachineImpl] objects. * Each such object represents an instantiation of a (two-party) flow that has reached a particular point. * * An implementation of this class will persist state machines to long term storage so they can survive process restarts @@ -75,9 +79,14 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, private val quasarKryoPool = KryoPool.Builder { val serializer = Fiber.getFiberSerializer(false) as KryoSerializer - DefaultKryoCustomizer.customize(serializer.kryo) - serializer.kryo.addDefaultSerializer(AutoCloseable::class.java, AutoCloseableSerialisationDetector) - serializer.kryo + val classResolver = makeNoWhitelistClassResolver().apply { setKryo(serializer.kryo) } + // TODO The ClassResolver can only be set in the Kryo constructor and Quasar doesn't provide us with a way of doing that + val field = Kryo::class.java.getDeclaredField("classResolver").apply { isAccessible = true } + serializer.kryo.apply { + field.set(this, classResolver) + DefaultKryoCustomizer.customize(this) + addDefaultSerializer(AutoCloseable::class.java, AutoCloseableSerialisationDetector) + } }.build() private object AutoCloseableSerialisationDetector : Serializer() { @@ -107,8 +116,6 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, } } - val scheduler = FiberScheduler() - sealed class Change { abstract val logic: FlowLogic<*> @@ -129,14 +136,18 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, } } + private val scheduler = FiberScheduler() private val mutex = ThreadBox(InnerState()) + // This thread (only enabled in dev mode) deserialises checkpoints in the background to shake out bugs in checkpoint restore. + private val checkpointCheckerThread = if (serviceHub.configuration.devMode) Executors.newSingleThreadExecutor() else null + + @Volatile private var unrestorableCheckpoints = false // True if we're shutting down, so don't resume anything. @Volatile private var stopping = false // How many Fibers are running and not suspended. If zero and stopping is true, then we are halted. private val liveFibers = ReusableLatch() - // Monitoring support. private val metrics = serviceHub.monitoringService.metrics @@ -225,6 +236,8 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, // Account for any expected Fibers in a test scenario. liveFibers.countDown(allowedUnsuspendedFiberCount) liveFibers.await() + checkpointCheckerThread?.let { MoreExecutors.shutdownAndAwaitTermination(it, 5, SECONDS) } + check(!unrestorableCheckpoints) { "Unrestorable checkpoints where created, please check the logs for details." } } /** @@ -239,12 +252,13 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, private fun restoreFibersFromCheckpoints() { mutex.locked { - checkpointStorage.forEach { + checkpointStorage.forEach { checkpoint -> // If a flow is added before start() then don't attempt to restore it - if (!stateMachines.containsValue(it)) { - val fiber = deserializeFiber(it) - initFiber(fiber) - stateMachines[fiber] = it + if (!stateMachines.containsValue(checkpoint)) { + deserializeFiber(checkpoint, logger)?.let { + initFiber(it) + stateMachines[it] = checkpoint + } } true } @@ -396,12 +410,17 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, } } - private fun deserializeFiber(checkpoint: Checkpoint): FlowStateMachineImpl<*> { - return quasarKryoPool.run { kryo -> - // put the map of token -> tokenized into the kryo context - kryo.withSerializationContext(serializationContext) { - checkpoint.serializedFiber.deserialize(kryo) - }.apply { fromCheckpoint = true } + private fun deserializeFiber(checkpoint: Checkpoint, logger: Logger): FlowStateMachineImpl<*>? { + return try { + quasarKryoPool.run { kryo -> + // put the map of token -> tokenized into the kryo context + kryo.withSerializationContext(serializationContext) { + checkpoint.serializedFiber.deserialize(kryo) + }.apply { fromCheckpoint = true } + } + } catch (t: Throwable) { + logger.error("Encountered unrestorable checkpoint!", t) + null } } @@ -508,6 +527,14 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, } checkpointStorage.addCheckpoint(newCheckpoint) checkpointingMeter.mark() + + checkpointCheckerThread?.execute { + // Immediately check that the checkpoint is valid by deserialising it. The idea is to plug any holes we have + // in our testing by failing any test where unrestorable checkpoints are created. + if (deserializeFiber(newCheckpoint, fiber.logger) == null) { + unrestorableCheckpoints = true + } + } } private fun resumeFiber(fiber: FlowStateMachineImpl<*>) { diff --git a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt index 7dc7cbfd09..d592019815 100644 --- a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt @@ -32,6 +32,7 @@ import net.corda.testing.node.MockNetwork.MockNode import net.corda.testing.sequence import org.apache.commons.io.IOUtils import org.assertj.core.api.Assertions.assertThatExceptionOfType +import org.junit.After import org.junit.Assert.assertArrayEquals import org.junit.Before import org.junit.Test @@ -76,6 +77,11 @@ class CordaRPCOpsImplTest { } } + @After + fun cleanUp() { + mockNet.stopNodes() + } + @Test fun `cash issue accepted`() { val quantity = 1000L diff --git a/node/src/test/kotlin/net/corda/node/messaging/AttachmentTests.kt b/node/src/test/kotlin/net/corda/node/messaging/AttachmentTests.kt index 6baac37959..a9fb24ce5b 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/AttachmentTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/AttachmentTests.kt @@ -17,6 +17,7 @@ import net.corda.node.utilities.transaction import net.corda.testing.node.MockNetwork import net.corda.testing.node.makeTestDataSourceProperties import org.jetbrains.exposed.sql.Database +import org.junit.After import org.junit.Before import org.junit.Test import java.io.ByteArrayInputStream @@ -38,12 +39,15 @@ class AttachmentTests { @Before fun setUp() { mockNet = MockNetwork() - val dataSourceProperties = makeTestDataSourceProperties() - configuration = RequeryConfiguration(dataSourceProperties) } + @After + fun cleanUp() { + mockNet.stopNodes() + } + fun fakeAttachment(): ByteArray { val bs = ByteArrayOutputStream() val js = JarOutputStream(bs) diff --git a/node/src/test/kotlin/net/corda/node/messaging/InMemoryMessagingTests.kt b/node/src/test/kotlin/net/corda/node/messaging/InMemoryMessagingTests.kt index 795f5d29fe..b935635105 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/InMemoryMessagingTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/InMemoryMessagingTests.kt @@ -7,6 +7,7 @@ import net.corda.node.services.messaging.TopicStringValidator import net.corda.node.services.messaging.createMessage import net.corda.node.services.network.NetworkMapService import net.corda.testing.node.MockNetwork +import org.junit.After import org.junit.Test import java.util.* import kotlin.test.assertEquals @@ -16,8 +17,13 @@ import kotlin.test.assertTrue class InMemoryMessagingTests { val mockNet = MockNetwork() + @After + fun cleanUp() { + mockNet.stopNodes() + } + @Test - fun topicStringValidation() { + fun `topic string validation`() { TopicStringValidator.check("this.is.ok") TopicStringValidator.check("this.is.OkAlso") assertFails { diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index 9425372851..8e4e9611ba 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -3,7 +3,6 @@ package net.corda.node.messaging import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.CommercialPaper import net.corda.contracts.asset.* -import net.corda.testing.contracts.fillWithSomeTestCash import net.corda.core.* import net.corda.core.contracts.* import net.corda.core.crypto.DigitalSignature @@ -27,7 +26,8 @@ import net.corda.core.serialization.serialize import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.* +import net.corda.core.utilities.LogHelper +import net.corda.core.utilities.unwrap import net.corda.flows.TwoPartyTradeFlow.Buyer import net.corda.flows.TwoPartyTradeFlow.Seller import net.corda.node.internal.AbstractNode @@ -37,6 +37,7 @@ import net.corda.node.services.persistence.DBTransactionStorage import net.corda.node.services.persistence.checkpoints import net.corda.node.utilities.transaction import net.corda.testing.* +import net.corda.testing.contracts.fillWithSomeTestCash import net.corda.testing.node.InMemoryMessagingNetwork import net.corda.testing.node.MockNetwork import org.assertj.core.api.Assertions.assertThat @@ -76,6 +77,7 @@ class TwoPartyTradeFlowTests { @After fun after() { + mockNet.stopNodes() LogHelper.reset("platform.trade", "core.contract.TransactionGroup", "recordingmap") } diff --git a/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt b/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt index 037368b2f3..693abdc855 100644 --- a/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt +++ b/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt @@ -25,6 +25,7 @@ import java.time.Clock open class MockServiceHubInternal( override val database: Database, + override val configuration: NodeConfiguration, val customVault: VaultService? = null, val customVaultQuery: VaultQueryService? = null, val keyManagement: KeyManagementService? = null, @@ -60,8 +61,6 @@ open class MockServiceHubInternal( get() = overrideClock ?: throw UnsupportedOperationException() override val myInfo: NodeInfo get() = throw UnsupportedOperationException() - override val configuration: NodeConfiguration - get() = throw UnsupportedOperationException() override val monitoringService: MonitoringService = MonitoringService(MetricRegistry()) override val rpcFlows: List>> get() = throw UnsupportedOperationException() diff --git a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt index b7fae46b64..b47ee4d0a0 100644 --- a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt @@ -8,16 +8,17 @@ import net.corda.core.identity.Party import net.corda.core.node.services.ServiceInfo import net.corda.core.seconds import net.corda.core.transactions.WireTransaction -import net.corda.testing.DUMMY_NOTARY import net.corda.flows.NotaryChangeFlow import net.corda.flows.StateReplacementException import net.corda.node.internal.AbstractNode import net.corda.node.services.network.NetworkMapService import net.corda.node.services.transactions.SimpleNotaryService +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.getTestPartyAndCertificate import net.corda.testing.node.MockNetwork import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.bouncycastle.asn1.x500.X500Name +import org.junit.After import org.junit.Before import org.junit.Test import java.time.Instant @@ -45,6 +46,11 @@ class NotaryChangeTests { mockNet.runNetwork() // Clear network map registration messages } + @After + fun cleanUp() { + mockNet.stopNodes() + } + @Test fun `should change notary for a state with single participant`() { val state = issueState(clientNodeA, oldNotaryNode) diff --git a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt index c0e87230e0..46ef15d5c8 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt @@ -21,10 +21,12 @@ import net.corda.node.services.vault.NodeVaultService import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction +import net.corda.testing.getTestX509Name import net.corda.testing.node.InMemoryMessagingNetwork import net.corda.testing.node.MockKeyManagementService import net.corda.testing.node.TestClock import net.corda.testing.node.makeTestDataSourceProperties +import net.corda.testing.testNodeConfiguration import org.assertj.core.api.Assertions.assertThat import org.bouncycastle.asn1.x500.X500Name import org.jetbrains.exposed.sql.Database @@ -32,6 +34,7 @@ import org.junit.After import org.junit.Before import org.junit.Test import java.io.Closeable +import java.nio.file.Paths import java.security.PublicKey import java.time.Clock import java.time.Instant @@ -67,7 +70,6 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { val testReference: NodeSchedulerServiceTest } - @Before fun setup() { countDown = CountDownLatch(1) @@ -87,7 +89,12 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { InMemoryMessagingNetwork.PeerHandle(0, nullIdentity), AffinityExecutor.ServiceAffinityExecutor("test", 1), database) - services = object : MockServiceHubInternal(database, overrideClock = testClock, keyManagement = kms, network = mockMessagingService), TestReference { + services = object : MockServiceHubInternal( + database, + testNodeConfiguration(Paths.get("."), getTestX509Name("Alice")), + overrideClock = testClock, + keyManagement = kms, + network = mockMessagingService), TestReference { override val vaultService: VaultService = NodeVaultService(this, dataSourceProps) override val testReference = this@NodeSchedulerServiceTest } diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DataVendingServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DataVendingServiceTests.kt index 8f6181b639..f912949506 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DataVendingServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DataVendingServiceTests.kt @@ -12,14 +12,15 @@ import net.corda.core.flows.InitiatingFlow import net.corda.core.identity.Party import net.corda.core.node.services.unconsumedStates import net.corda.core.transactions.SignedTransaction -import net.corda.testing.DUMMY_NOTARY import net.corda.flows.BroadcastTransactionFlow.NotifyTxRequest import net.corda.node.services.NotifyTransactionHandler import net.corda.node.utilities.transaction +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.MEGA_CORP import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork.MockNode import org.assertj.core.api.Assertions.assertThat +import org.junit.After import org.junit.Before import org.junit.Test import kotlin.test.assertEquals @@ -35,6 +36,11 @@ class DataVendingServiceTests { mockNet = MockNetwork() } + @After + fun cleanUp() { + mockNet.stopNodes() + } + @Test fun `notify of transaction`() { val (vaultServiceNode, registerNode) = mockNet.createTwoNodes() diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt index 54f97ba873..1ed12b9fd3 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt @@ -54,6 +54,7 @@ import org.junit.Before import org.junit.Test import rx.Notification import rx.Observable +import java.time.Instant import java.util.* import kotlin.reflect.KClass import kotlin.test.assertEquals @@ -106,11 +107,7 @@ class FlowFrameworkTests { @Test fun `flow can lazily use the serviceHub in its constructor`() { - val flow = object : FlowLogic() { - val lazyTime by lazy { serviceHub.clock.instant() } - @Suspendable - override fun call() = Unit - } + val flow = LazyServiceHubAccessFlow() node1.services.startFlow(flow) assertThat(flow.lazyTime).isNotNull() } @@ -754,6 +751,12 @@ class FlowFrameworkTests { .toFuture() } + private class LazyServiceHubAccessFlow : FlowLogic() { + val lazyTime: Instant by lazy { serviceHub.clock.instant() } + @Suspendable + override fun call() = Unit + } + private class NoOpFlow(val nonTerminating: Boolean = false) : FlowLogic() { @Transient var flowStarted = false diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt index 4e50c6e75e..a553fb8b26 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt @@ -10,14 +10,15 @@ import net.corda.core.getOrThrow import net.corda.core.node.services.ServiceInfo import net.corda.core.seconds import net.corda.core.transactions.SignedTransaction -import net.corda.testing.DUMMY_NOTARY import net.corda.flows.NotaryError import net.corda.flows.NotaryException import net.corda.flows.NotaryFlow import net.corda.node.internal.AbstractNode import net.corda.node.services.network.NetworkMapService +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.node.MockNetwork import org.assertj.core.api.Assertions.assertThat +import org.junit.After import org.junit.Before import org.junit.Test import java.time.Instant @@ -30,7 +31,8 @@ class NotaryServiceTests { lateinit var notaryNode: MockNetwork.MockNode lateinit var clientNode: MockNetwork.MockNode - @Before fun setup() { + @Before + fun setup() { mockNet = MockNetwork() notaryNode = mockNet.createNode( legalName = DUMMY_NOTARY.name, @@ -39,7 +41,13 @@ class NotaryServiceTests { mockNet.runNetwork() // Clear network map registration messages } - @Test fun `should sign a unique transaction with a valid time-window`() { + @After + fun cleanUp() { + mockNet.stopNodes() + } + + @Test + fun `should sign a unique transaction with a valid time-window`() { val stx = run { val inputState = issueState(clientNode) val tx = TransactionType.General.Builder(notaryNode.info.notaryIdentity).withItems(inputState) @@ -52,7 +60,8 @@ class NotaryServiceTests { signatures.forEach { it.verify(stx.id) } } - @Test fun `should sign a unique transaction without a time-window`() { + @Test + fun `should sign a unique transaction without a time-window`() { val stx = run { val inputState = issueState(clientNode) val tx = TransactionType.General.Builder(notaryNode.info.notaryIdentity).withItems(inputState) @@ -64,7 +73,8 @@ class NotaryServiceTests { signatures.forEach { it.verify(stx.id) } } - @Test fun `should report error for transaction with an invalid time-window`() { + @Test + fun `should report error for transaction with an invalid time-window`() { val stx = run { val inputState = issueState(clientNode) val tx = TransactionType.General.Builder(notaryNode.info.notaryIdentity).withItems(inputState) @@ -78,7 +88,8 @@ class NotaryServiceTests { assertThat(ex.error).isInstanceOf(NotaryError.TimeWindowInvalid::class.java) } - @Test fun `should sign identical transaction multiple times (signing is idempotent)`() { + @Test + fun `should sign identical transaction multiple times (signing is idempotent)`() { val stx = run { val inputState = issueState(clientNode) val tx = TransactionType.General.Builder(notaryNode.info.notaryIdentity).withItems(inputState) @@ -95,7 +106,8 @@ class NotaryServiceTests { assertEquals(f1.resultFuture.getOrThrow(), f2.resultFuture.getOrThrow()) } - @Test fun `should report conflict when inputs are reused across transactions`() { + @Test + fun `should report conflict when inputs are reused across transactions`() { val inputState = issueState(clientNode) val stx = run { val tx = TransactionType.General.Builder(notaryNode.info.notaryIdentity).withItems(inputState) diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt index 9b735f825a..05bfb68c69 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt @@ -10,16 +10,17 @@ import net.corda.core.crypto.DigitalSignature import net.corda.core.getOrThrow import net.corda.core.node.services.ServiceInfo import net.corda.core.transactions.SignedTransaction -import net.corda.testing.DUMMY_NOTARY import net.corda.flows.NotaryError import net.corda.flows.NotaryException import net.corda.flows.NotaryFlow import net.corda.node.internal.AbstractNode import net.corda.node.services.issueInvalidState import net.corda.node.services.network.NetworkMapService +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.MEGA_CORP_KEY import net.corda.testing.node.MockNetwork import org.assertj.core.api.Assertions.assertThat +import org.junit.After import org.junit.Before import org.junit.Test import java.util.* @@ -31,7 +32,8 @@ class ValidatingNotaryServiceTests { lateinit var notaryNode: MockNetwork.MockNode lateinit var clientNode: MockNetwork.MockNode - @Before fun setup() { + @Before + fun setup() { mockNet = MockNetwork() notaryNode = mockNet.createNode( legalName = DUMMY_NOTARY.name, @@ -41,7 +43,13 @@ class ValidatingNotaryServiceTests { mockNet.runNetwork() // Clear network map registration messages } - @Test fun `should report error for invalid transaction dependency`() { + @After + fun cleanUp() { + mockNet.stopNodes() + } + + @Test + fun `should report error for invalid transaction dependency`() { val stx = run { val inputState = issueInvalidState(clientNode, notaryNode.info.notaryIdentity) val tx = TransactionType.General.Builder(notaryNode.info.notaryIdentity).withItems(inputState) @@ -54,7 +62,8 @@ class ValidatingNotaryServiceTests { assertThat(ex.error).isInstanceOf(NotaryError.SignaturesInvalid::class.java) } - @Test fun `should report error for missing signatures`() { + @Test + fun `should report error for missing signatures`() { val expectedMissingKey = MEGA_CORP_KEY.public val stx = run { val inputState = issueState(clientNode) diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt index e040a73a71..d9113124ae 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt @@ -14,16 +14,12 @@ import net.corda.core.getOrThrow import net.corda.core.identity.Party import net.corda.core.node.services.ServiceInfo import net.corda.core.transactions.TransactionBuilder -import net.corda.testing.ALICE -import net.corda.testing.DUMMY_NOTARY import net.corda.core.utilities.LogHelper import net.corda.core.utilities.ProgressTracker import net.corda.irs.flows.RatesFixFlow import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction -import net.corda.testing.ALICE_PUBKEY -import net.corda.testing.MEGA_CORP -import net.corda.testing.MEGA_CORP_KEY +import net.corda.testing.* import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockServices import net.corda.testing.node.makeTestDataSourceProperties @@ -232,6 +228,7 @@ class NodeInterestRatesTest { val fix = tx.toWireTransaction().commands.map { it.value as Fix }.first() assertEquals(fixOf, fix.of) assertEquals("0.678".bd, fix.value) + mockNet.stopNodes() } class FilteredRatesFlow(tx: TransactionBuilder, diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt index 2940891179..2d57e1f06e 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -21,7 +21,6 @@ import net.corda.core.node.services.IdentityService import net.corda.core.node.services.KeyManagementService import net.corda.core.node.services.ServiceInfo import net.corda.core.utilities.loggerFor -import net.corda.flows.TransactionKeyFlow import net.corda.node.internal.AbstractNode import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.identity.InMemoryIdentityService @@ -413,7 +412,6 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, } fun stopNodes() { - require(nodes.isNotEmpty()) nodes.forEach { if (it.started) it.stop() } } From 81b84ebf5c1c99f9012c033bc4db734e48e76e46 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Thu, 6 Jul 2017 14:09:14 +0100 Subject: [PATCH 61/97] Review comments Name Exceptions Exception Swap null / non null annotations onto the correct classes Don't shadow parameters with local vars Explicitly handle Character Type --- .../serialization/carpenter/ClassCarpenter.kt | 33 ++++++++++++------- .../carpenter/ClassCarpenterTest.kt | 6 ++-- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/serialization/carpenter/ClassCarpenter.kt b/core/src/main/kotlin/net/corda/core/serialization/carpenter/ClassCarpenter.kt index 5a2819b0f4..b433cb417a 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/carpenter/ClassCarpenter.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/carpenter/ClassCarpenter.kt @@ -68,12 +68,15 @@ class ClassCarpenter { // TODO: Support annotations. // TODO: isFoo getter patterns for booleans (this is what Kotlin generates) - class DuplicateName : RuntimeException("An attempt was made to register two classes with the same name within the same ClassCarpenter namespace.") - class InterfaceMismatch(msg: String) : RuntimeException(msg) - class NullablePrimitive(msg: String) : RuntimeException(msg) + class DuplicateNameException : RuntimeException("An attempt was made to register two classes with the same name within the same ClassCarpenter namespace.") + class InterfaceMismatchException(msg: String) : RuntimeException(msg) + class NullablePrimitiveException(msg: String) : RuntimeException(msg) abstract class Field(val field: Class) { - protected val unsetName = "Unset" + companion object { + const val unsetName = "Unset" + } + var name: String = unsetName abstract val nullabilityAnnotation: String @@ -107,7 +110,7 @@ class ClassCarpenter { } class NonNullableField(field: Class) : Field(field) { - override val nullabilityAnnotation = "Ljavax/annotations/Nullable;" + override val nullabilityAnnotation = "Ljavax/annotations/NotNull;" constructor(name: String, field: Class) : this(field) { this.name = name @@ -135,11 +138,11 @@ class ClassCarpenter { class NullableField(field: Class) : Field(field) { - override val nullabilityAnnotation = "Ljavax/annotations/NotNull;" + override val nullabilityAnnotation = "Ljavax/annotations/Nullable;" constructor(name: String, field: Class) : this(field) { if (field.isPrimitive) { - throw NullablePrimitive ( + throw NullablePrimitiveException ( "Field $name is primitive type ${Type.getDescriptor(field)} and thus cannot be nullable") } @@ -329,7 +332,8 @@ class ClassCarpenter { visitVarInsn(ALOAD, 0) // Load 'this' visitFieldInsn(GETFIELD, schema.jvmName, name, type.descriptor) when (type.field) { - java.lang.Boolean.TYPE, Integer.TYPE, java.lang.Short.TYPE, java.lang.Byte.TYPE, TYPE -> visitInsn(IRETURN) + java.lang.Boolean.TYPE, Integer.TYPE, java.lang.Short.TYPE, java.lang.Byte.TYPE, + java.lang.Character.TYPE, TYPE -> visitInsn(IRETURN) java.lang.Long.TYPE -> visitInsn(LRETURN) java.lang.Double.TYPE -> visitInsn(DRETURN) java.lang.Float.TYPE -> visitInsn(FRETURN) @@ -394,7 +398,8 @@ class ClassCarpenter { private fun MethodVisitor.load(slot: Int, type: Field): Int { when (type.field) { - java.lang.Boolean.TYPE, Integer.TYPE, java.lang.Short.TYPE, java.lang.Byte.TYPE, TYPE -> visitVarInsn(ILOAD, slot) + java.lang.Boolean.TYPE, Integer.TYPE, java.lang.Short.TYPE, java.lang.Byte.TYPE, + java.lang.Character.TYPE, TYPE -> visitVarInsn(ILOAD, slot) java.lang.Long.TYPE -> visitVarInsn(LLOAD, slot) java.lang.Double.TYPE -> visitVarInsn(DLOAD, slot) java.lang.Float.TYPE -> visitVarInsn(FLOAD, slot) @@ -407,7 +412,7 @@ class ClassCarpenter { } private fun validateSchema(schema: Schema) { - if (schema.name in _loaded) throw DuplicateName() + if (schema.name in _loaded) throw DuplicateNameException() fun isJavaName(n: String) = n.isNotBlank() && isJavaIdentifierStart(n.first()) && n.all(::isJavaIdentifierPart) require(isJavaName(schema.name.split(".").last())) { "Not a valid Java name: ${schema.name}" } schema.fields.keys.forEach { require(isJavaName(it)) { "Not a valid Java name: $it" } } @@ -419,11 +424,15 @@ class ClassCarpenter { itf.methods.forEach { val fieldNameFromItf = when { it.name.startsWith("get") -> it.name.substring(3).decapitalize() - else -> throw InterfaceMismatch("Requested interfaces must consist only of methods that start with 'get': ${itf.name}.${it.name}") + else -> throw InterfaceMismatchException( + "Requested interfaces must consist only of methods that start " + + "with 'get': ${itf.name}.${it.name}") } if ((schema is ClassSchema) and (fieldNameFromItf !in allFields)) - throw InterfaceMismatch("Interface ${itf.name} requires a field named $fieldNameFromItf but that isn't found in the schema or any superclass schemas") + throw InterfaceMismatchException( + "Interface ${itf.name} requires a field named $fieldNameFromItf but that " + + "isn't found in the schema or any superclass schemas") } } } diff --git a/core/src/test/kotlin/net/corda/core/serialization/carpenter/ClassCarpenterTest.kt b/core/src/test/kotlin/net/corda/core/serialization/carpenter/ClassCarpenterTest.kt index 8baa7fd4ae..ef9d75f640 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/carpenter/ClassCarpenterTest.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/carpenter/ClassCarpenterTest.kt @@ -90,7 +90,7 @@ class ClassCarpenterTest { assertEquals("Person{age=32, name=Mike}", i.toString()) } - @Test(expected = ClassCarpenter.DuplicateName::class) + @Test(expected = ClassCarpenter.DuplicateNameException::class) fun duplicates() { cc.build(ClassCarpenter.ClassSchema("gen.EmptyClass", emptyMap(), null)) cc.build(ClassCarpenter.ClassSchema("gen.EmptyClass", emptyMap(), null)) @@ -140,7 +140,7 @@ class ClassCarpenterTest { assertEquals(1, i.b) } - @Test(expected = ClassCarpenter.InterfaceMismatch::class) + @Test(expected = ClassCarpenter.InterfaceMismatchException::class) fun `mismatched interface`() { val schema1 = ClassCarpenter.ClassSchema( "gen.A", @@ -268,7 +268,7 @@ class ClassCarpenterTest { clazz.constructors[0].newInstance(a) } - @Test(expected = ClassCarpenter.NullablePrimitive::class) + @Test(expected = ClassCarpenter.NullablePrimitiveException::class) fun `nullable parameter small int`() { val className = "iEnjoySwede" val schema = ClassCarpenter.ClassSchema( From 3063debd98d441ef09a4d9f09c83c7c9fd78e3a2 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Thu, 6 Jul 2017 14:23:43 +0100 Subject: [PATCH 62/97] Moves to builder syntax for TxBuilder. Adds attachments and time-windows to withItems. --- .../corda/core/contracts/TransactionTypes.kt | 3 +- .../core/transactions/TransactionBuilder.kt | 74 ++++++++++--------- .../kotlin/net/corda/docs/FlowCookbook.kt | 2 +- .../main/kotlin/net/corda/testing/TestDSL.kt | 5 +- 4 files changed, 44 insertions(+), 40 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/contracts/TransactionTypes.kt b/core/src/main/kotlin/net/corda/core/contracts/TransactionTypes.kt index b1e98a49e9..5a2fbdb917 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TransactionTypes.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TransactionTypes.kt @@ -147,9 +147,10 @@ sealed class TransactionType { * and adds the list of participants to the signers set for every input state. */ class Builder(notary: Party) : TransactionBuilder(NotaryChange, notary) { - override fun addInputState(stateAndRef: StateAndRef<*>) { + override fun addInputState(stateAndRef: StateAndRef<*>): TransactionBuilder { signers.addAll(stateAndRef.state.data.participants.map { it.owningKey }) super.addInputState(stateAndRef) + return this } } 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 b9d4574ecf..61a42d2dbb 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -1,11 +1,10 @@ package net.corda.core.transactions import co.paralleluniverse.strands.Strand -import com.google.common.annotations.VisibleForTesting import net.corda.core.contracts.* import net.corda.core.crypto.* -import net.corda.core.internal.FlowStateMachine import net.corda.core.identity.Party +import net.corda.core.internal.FlowStateMachine import net.corda.core.node.ServiceHub import net.corda.core.serialization.serialize import java.security.KeyPair @@ -40,23 +39,22 @@ open class TransactionBuilder( protected val outputs: MutableList> = arrayListOf(), protected val commands: MutableList = arrayListOf(), protected val signers: MutableSet = mutableSetOf(), - window: TimeWindow? = null) { + protected var window: TimeWindow? = null) { constructor(type: TransactionType, notary: Party) : this(type, notary, (Strand.currentStrand() as? FlowStateMachine<*>)?.id?.uuid ?: UUID.randomUUID()) /** * Creates a copy of the builder. */ - fun copy(): TransactionBuilder = - TransactionBuilder( - type = type, - notary = notary, - inputs = ArrayList(inputs), - attachments = ArrayList(attachments), - outputs = ArrayList(outputs), - commands = ArrayList(commands), - signers = LinkedHashSet(signers), - window = timeWindow - ) + fun copy() = TransactionBuilder( + type = type, + notary = notary, + inputs = ArrayList(inputs), + attachments = ArrayList(attachments), + outputs = ArrayList(outputs), + commands = ArrayList(commands), + signers = LinkedHashSet(signers), + window = window + ) // DOCSTART 1 /** A more convenient way to add items to this transaction that calls the add* methods for you based on type */ @@ -64,10 +62,12 @@ open class TransactionBuilder( for (t in items) { when (t) { is StateAndRef<*> -> addInputState(t) + is SecureHash -> addAttachment(t) is TransactionState<*> -> addOutputState(t) is ContractState -> addOutputState(t) is Command -> addCommand(t) is CommandData -> throw IllegalArgumentException("You passed an instance of CommandData, but that lacks the pubkey. You need to wrap it in a Command object first.") + is TimeWindow -> setTimeWindow(t) else -> throw IllegalArgumentException("Wrong argument type: ${t.javaClass}") } } @@ -76,7 +76,7 @@ open class TransactionBuilder( // DOCEND 1 fun toWireTransaction() = WireTransaction(ArrayList(inputs), ArrayList(attachments), - ArrayList(outputs), ArrayList(commands), notary, signers.toList(), type, timeWindow) + ArrayList(outputs), ArrayList(commands), notary, signers.toList(), type, window) @Throws(AttachmentResolutionException::class, TransactionResolutionException::class) fun toLedgerTransaction(services: ServiceHub) = toWireTransaction().toLedgerTransaction(services) @@ -86,51 +86,55 @@ open class TransactionBuilder( toLedgerTransaction(services).verify() } - open fun addInputState(stateAndRef: StateAndRef<*>) { + open fun addInputState(stateAndRef: StateAndRef<*>): TransactionBuilder { val notary = stateAndRef.state.notary require(notary == this.notary) { "Input state requires notary \"$notary\" which does not match the transaction notary \"${this.notary}\"." } signers.add(notary.owningKey) inputs.add(stateAndRef.ref) + return this } - fun addAttachment(attachmentId: SecureHash) { + fun addAttachment(attachmentId: SecureHash): TransactionBuilder { attachments.add(attachmentId) + return this } - fun addOutputState(state: TransactionState<*>): Int { + fun addOutputState(state: TransactionState<*>): TransactionBuilder { outputs.add(state) - return outputs.size - 1 + return this } @JvmOverloads fun addOutputState(state: ContractState, notary: Party, encumbrance: Int? = null) = addOutputState(TransactionState(state, notary, encumbrance)) /** A default notary must be specified during builder construction to use this method */ - fun addOutputState(state: ContractState): Int { + fun addOutputState(state: ContractState): TransactionBuilder { checkNotNull(notary) { "Need to specify a notary for the state, or set a default one on TransactionBuilder initialisation" } - return addOutputState(state, notary!!) + addOutputState(state, notary!!) + return this } - fun addCommand(arg: Command) { + fun addCommand(arg: Command): TransactionBuilder { // TODO: replace pubkeys in commands with 'pointers' to keys in signers signers.addAll(arg.signers) commands.add(arg) + return this } fun addCommand(data: CommandData, vararg keys: PublicKey) = addCommand(Command(data, listOf(*keys))) fun addCommand(data: CommandData, keys: List) = addCommand(Command(data, keys)) - var timeWindow: TimeWindow? = window - /** - * Sets the [TimeWindow] for this transaction, replacing the existing [TimeWindow] if there is one. To be valid, the - * transaction must then be signed by the notary service within this window of time. In this way, the notary acts as - * the Timestamp Authority. - */ - set(value) { - check(notary != null) { "Only notarised transactions can have a time-window" } - signers.add(notary!!.owningKey) - field = value - } + /** + * Sets the [TimeWindow] for this transaction, replacing the existing [TimeWindow] if there is one. To be valid, the + * transaction must then be signed by the notary service within this window of time. In this way, the notary acts as + * the Timestamp Authority. + */ + fun setTimeWindow(timeWindow: TimeWindow): TransactionBuilder { + check(notary != null) { "Only notarised transactions can have a time-window" } + signers.add(notary!!.owningKey) + window = timeWindow + return this + } /** * The [TimeWindow] for the transaction can also be defined as [time] +/- [timeTolerance]. The tolerance should be @@ -139,9 +143,7 @@ open class TransactionBuilder( * collaborating parties may therefore require a higher time tolerance than a transaction being built by a single * node. */ - fun setTimeWindow(time: Instant, timeTolerance: Duration) { - timeWindow = TimeWindow.withTolerance(time, timeTolerance) - } + fun setTimeWindow(time: Instant, timeTolerance: Duration) = setTimeWindow(TimeWindow.withTolerance(time, timeTolerance)) // Accessors that yield immutable snapshots. fun inputStates(): List = ArrayList(inputs) diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt index bff2303dcf..677b9b12b9 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt @@ -289,7 +289,7 @@ object FlowCookbook { regTxBuilder.addAttachment(ourAttachment) // We set the time-window within which the transaction must be notarised using either of: - regTxBuilder.timeWindow = ourTimeWindow + regTxBuilder.setTimeWindow(ourTimeWindow) regTxBuilder.setTimeWindow(serviceHub.clock.instant(), Duration.ofSeconds(30)) /**---------------------- diff --git a/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt b/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt index faaefa8653..3755f92b22 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt @@ -116,11 +116,12 @@ data class TestTransactionDSLInterpreter private constructor( } override fun _output(label: String?, notary: Party, encumbrance: Int?, contractState: ContractState) { - val outputIndex = transactionBuilder.addOutputState(contractState, notary, encumbrance) + transactionBuilder.addOutputState(contractState, notary, encumbrance) if (label != null) { if (label in labelToIndexMap) { throw DuplicateOutputLabel(label) } else { + val outputIndex = transactionBuilder.outputStates().size - 1 labelToIndexMap[label] = outputIndex } } @@ -145,7 +146,7 @@ data class TestTransactionDSLInterpreter private constructor( } override fun timeWindow(data: TimeWindow) { - transactionBuilder.timeWindow = data + transactionBuilder.setTimeWindow(data) } override fun tweak( From 2ba34de460c10852300af409aa6ff881ad41efc6 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Thu, 6 Jul 2017 14:23:52 +0100 Subject: [PATCH 63/97] Fix broken commands. --- docs/source/hello-world-template.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/hello-world-template.rst b/docs/source/hello-world-template.rst index c298fb87c0..d38350571f 100644 --- a/docs/source/hello-world-template.rst +++ b/docs/source/hello-world-template.rst @@ -24,13 +24,13 @@ Open a terminal window in the directory where you want to download the CorDapp t .. code-block:: text # Clone the template from GitHub: - git clone https://github.com/corda/cordapp-template.git & cd cordapp-template + git clone https://github.com/corda/cordapp-template.git ; cd cordapp-template # Retrieve a list of the stable Milestone branches using: git branch -a --list *release-M* # Check out the Milestone branch with the latest version number: - git checkout release-M[*version number*] & git pull + git checkout release-M[*version number*] ; git pull Template structure ------------------ @@ -82,4 +82,4 @@ Progress so far --------------- We now have a template that we can build upon to define our IOU CorDapp. -We'll begin writing the CorDapp proper by writing the definition of the ``IOUState``. \ No newline at end of file +We'll begin writing the CorDapp proper by writing the definition of the ``IOUState``. From 8fc76b3803964448ceee5348913c39e5e1833626 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Thu, 6 Jul 2017 10:43:45 +0100 Subject: [PATCH 64/97] Moved VersionInfo out of core and into node --- .../net/corda/services/messaging/P2PSecurityTest.kt | 2 +- .../src/main/kotlin/net/corda}/node/VersionInfo.kt | 2 +- node/src/main/kotlin/net/corda/node/internal/Node.kt | 2 +- .../main/kotlin/net/corda/node/internal/NodeStartup.kt | 2 +- .../corda/node/services/messaging/NodeMessagingClient.kt | 2 +- .../node/services/messaging/ArtemisMessagingTests.kt | 4 ++-- .../services/network/PersistentNetworkMapServiceTest.kt | 5 ++--- .../src/main/kotlin/net/corda/testing/CoreTestUtils.kt | 2 -- .../src/main/kotlin/net/corda/testing/driver/Driver.kt | 9 +++++++-- .../kotlin/net/corda/testing/node/MockNetworkMapCache.kt | 5 ++--- .../src/main/kotlin/net/corda/testing/node/MockNode.kt | 2 +- .../main/kotlin/net/corda/testing/node/MockServices.kt | 8 +++++--- .../main/kotlin/net/corda/testing/node/NodeBasedTest.kt | 3 +-- .../src/main/kotlin/net/corda/testing/node/SimpleNode.kt | 1 - 14 files changed, 25 insertions(+), 24 deletions(-) rename {core/src/main/kotlin/net/corda/core => node/src/main/kotlin/net/corda}/node/VersionInfo.kt (91%) diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt index a5789b3b7b..3df678b855 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt @@ -67,7 +67,7 @@ class P2PSecurityTest : NodeBasedTest() { private fun SimpleNode.registerWithNetworkMap(registrationName: X500Name): ListenableFuture { val legalIdentity = getTestPartyAndCertificate(registrationName, identity.public) - val nodeInfo = NodeInfo(listOf(MOCK_HOST_AND_PORT), legalIdentity, setOf(legalIdentity), MOCK_VERSION_INFO.platformVersion) + val nodeInfo = NodeInfo(listOf(MOCK_HOST_AND_PORT), legalIdentity, setOf(legalIdentity), 1) val registration = NodeRegistration(nodeInfo, System.currentTimeMillis(), AddOrRemove.ADD, Instant.MAX) val request = RegistrationRequest(registration.toWire(keyService, identity.public), network.myAddress) return network.sendRequest(NetworkMapService.REGISTER_TOPIC, request, networkMapNode.network.myAddress) diff --git a/core/src/main/kotlin/net/corda/core/node/VersionInfo.kt b/node/src/main/kotlin/net/corda/node/VersionInfo.kt similarity index 91% rename from core/src/main/kotlin/net/corda/core/node/VersionInfo.kt rename to node/src/main/kotlin/net/corda/node/VersionInfo.kt index f072eafe04..c51df32229 100644 --- a/core/src/main/kotlin/net/corda/core/node/VersionInfo.kt +++ b/node/src/main/kotlin/net/corda/node/VersionInfo.kt @@ -1,4 +1,4 @@ -package net.corda.core.node +package net.corda.node /** * Encapsulates various pieces of version information of the node. diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index 993fe6391b..871bdd6307 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -9,12 +9,12 @@ import net.corda.core.flatMap import net.corda.core.messaging.RPCOps import net.corda.core.minutes import net.corda.core.node.ServiceHub -import net.corda.core.node.VersionInfo import net.corda.core.node.services.ServiceInfo import net.corda.core.seconds import net.corda.core.success import net.corda.core.utilities.loggerFor import net.corda.core.utilities.trace +import net.corda.node.VersionInfo import net.corda.node.serialization.NodeClock import net.corda.node.services.RPCUserService import net.corda.node.services.RPCUserServiceImpl diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt index 2251244e0b..b9824e7c6d 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -6,7 +6,7 @@ import joptsimple.OptionException import net.corda.core.* import net.corda.core.crypto.commonName import net.corda.core.crypto.orgName -import net.corda.core.node.VersionInfo +import net.corda.node.VersionInfo import net.corda.core.node.services.ServiceInfo import net.corda.core.utilities.Emoji import net.corda.core.utilities.loggerFor diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt index d3773f68b7..56afb69dd3 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt @@ -7,13 +7,13 @@ import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.MessageRecipients import net.corda.core.messaging.RPCOps import net.corda.core.messaging.SingleMessageRecipient -import net.corda.core.node.VersionInfo import net.corda.core.node.services.PartyInfo import net.corda.core.node.services.TransactionVerifierService import net.corda.core.serialization.opaque import net.corda.core.transactions.LedgerTransaction import net.corda.core.utilities.loggerFor import net.corda.core.utilities.trace +import net.corda.node.VersionInfo import net.corda.node.services.RPCUserService import net.corda.node.services.api.MonitoringService import net.corda.node.services.config.NodeConfiguration diff --git a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt index de4aaf1c0f..4a4a0cc7e3 100644 --- a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt @@ -8,7 +8,6 @@ import com.google.common.util.concurrent.SettableFuture import net.corda.core.crypto.generateKeyPair import net.corda.core.messaging.RPCOps import net.corda.core.node.services.DEFAULT_SESSION_ID -import net.corda.testing.ALICE import net.corda.core.utilities.LogHelper import net.corda.node.services.RPCUserService import net.corda.node.services.RPCUserServiceImpl @@ -21,9 +20,10 @@ import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction -import net.corda.testing.MOCK_VERSION_INFO +import net.corda.testing.ALICE import net.corda.testing.freeLocalHostAndPort import net.corda.testing.freePort +import net.corda.testing.node.MOCK_VERSION_INFO import net.corda.testing.node.makeTestDataSourceProperties import net.corda.testing.testNodeConfiguration import org.assertj.core.api.Assertions.assertThat diff --git a/node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapServiceTest.kt index 2a3a8fd3b2..c5af8af7ae 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapServiceTest.kt @@ -5,7 +5,6 @@ import net.corda.core.node.services.ServiceInfo import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.config.NodeConfiguration import net.corda.node.utilities.transaction -import net.corda.testing.MOCK_VERSION_INFO import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork.MockNode import java.math.BigInteger @@ -49,11 +48,11 @@ class PersistentNetworkMapServiceTest : AbstractNetworkMapServiceTest get() = listOf(MEGA_CORP_KEY, MINI_CORP_KEY, AL val MOCK_IDENTITIES = listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_NOTARY_IDENTITY) val MOCK_IDENTITY_SERVICE: IdentityService get() = InMemoryIdentityService(MOCK_IDENTITIES, emptyMap(), DUMMY_CA.certificate.cert) -val MOCK_VERSION_INFO = VersionInfo(1, "Mock release", "Mock revision", "Mock Vendor") val MOCK_HOST_AND_PORT = HostAndPort.fromParts("mockHost", 30000) fun generateStateRef() = StateRef(SecureHash.randomSHA256(), 0) diff --git a/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt b/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt index 36d496d187..3d21d4c827 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -20,7 +20,8 @@ import net.corda.core.messaging.CordaRPCOps import net.corda.core.node.NodeInfo import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceType -import net.corda.core.utilities.* +import net.corda.core.utilities.WHITESPACE +import net.corda.core.utilities.loggerFor import net.corda.node.internal.Node import net.corda.node.internal.NodeStartup import net.corda.node.serialization.NodeClock @@ -34,7 +35,11 @@ import net.corda.nodeapi.User import net.corda.nodeapi.config.SSLConfiguration import net.corda.nodeapi.config.parseAs import net.corda.nodeapi.internal.addShutdownHook -import net.corda.testing.* +import net.corda.testing.ALICE +import net.corda.testing.BOB +import net.corda.testing.DUMMY_BANK_A +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.node.MOCK_VERSION_INFO import okhttp3.OkHttpClient import okhttp3.Request import org.bouncycastle.asn1.x500.X500Name diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt index 36a72e77f0..c4de528dd1 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt @@ -8,7 +8,6 @@ import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub import net.corda.core.node.services.NetworkMapCache import net.corda.node.services.network.InMemoryNetworkMapCache -import net.corda.testing.MOCK_VERSION_INFO import net.corda.testing.getTestPartyAndCertificate import net.corda.testing.getTestX509Name import rx.Observable @@ -29,8 +28,8 @@ class MockNetworkMapCache(serviceHub: ServiceHub) : InMemoryNetworkMapCache(serv override val changed: Observable = PublishSubject.create() init { - val mockNodeA = NodeInfo(listOf(BANK_C_ADDR), BANK_C, setOf(BANK_C), MOCK_VERSION_INFO.platformVersion) - val mockNodeB = NodeInfo(listOf(BANK_D_ADDR), BANK_D, setOf(BANK_D), MOCK_VERSION_INFO.platformVersion) + val mockNodeA = NodeInfo(listOf(BANK_C_ADDR), BANK_C, setOf(BANK_C), 1) + val mockNodeB = NodeInfo(listOf(BANK_D_ADDR), BANK_D, setOf(BANK_D), 1) registeredNodes[mockNodeA.legalIdentity.owningKey] = mockNodeA registeredNodes[mockNodeB.legalIdentity.owningKey] = mockNodeB runWithoutMapService() diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt index 2d57e1f06e..05f3c115be 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -144,7 +144,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, AbstractNode(config, advertisedServices, TestClock(), mockNet.busyLatch) { var counter = entropyRoot override val log: Logger = loggerFor() - override val platformVersion: Int get() = MOCK_VERSION_INFO.platformVersion + override val platformVersion: Int get() = 1 override val serverThread: AffinityExecutor = if (mockNet.threadPerNode) ServiceAffinityExecutor("Mock node $id thread", 1) diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt index 5422973d68..804e8520b7 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -12,8 +12,8 @@ import net.corda.core.node.services.* import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.SignedTransaction -import net.corda.testing.DUMMY_CA import net.corda.flows.AnonymisedIdentity +import net.corda.node.VersionInfo import net.corda.node.services.api.StateMachineRecordedTransactionMappingStorage import net.corda.node.services.api.WritableTransactionStorage import net.corda.node.services.database.HibernateConfiguration @@ -25,9 +25,9 @@ import net.corda.node.services.schema.HibernateObserver import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.transactions.InMemoryTransactionVerifierService import net.corda.node.services.vault.NodeVaultService +import net.corda.testing.DUMMY_CA import net.corda.testing.MEGA_CORP import net.corda.testing.MOCK_IDENTITIES -import net.corda.testing.MOCK_VERSION_INFO import net.corda.testing.getTestPartyAndCertificate import org.bouncycastle.operator.ContentSigner import rx.Observable @@ -76,7 +76,7 @@ open class MockServices(vararg val keys: KeyPair) : ServiceHub { override val clock: Clock get() = Clock.systemUTC() override val myInfo: NodeInfo get() { val identity = getTestPartyAndCertificate(MEGA_CORP.name, key.public) - return NodeInfo(listOf(HostAndPort.fromHost("localhost")), identity, setOf(identity), MOCK_VERSION_INFO.platformVersion) + return NodeInfo(listOf(HostAndPort.fromHost("localhost")), identity, setOf(identity), 1) } override val transactionVerifierService: TransactionVerifierService get() = InMemoryTransactionVerifierService(2) @@ -195,3 +195,5 @@ fun makeTestDataSourceProperties(nodeName: String = SecureHash.randomSHA256().to props.setProperty("dataSource.password", "") return props } + +val MOCK_VERSION_INFO = VersionInfo(1, "Mock release", "Mock revision", "Mock Vendor") diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt b/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt index f8f71c8277..81b11bd7fc 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt @@ -9,7 +9,6 @@ import net.corda.core.crypto.appendToCommonName import net.corda.core.crypto.commonName import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceType -import net.corda.testing.DUMMY_MAP import net.corda.core.utilities.WHITESPACE import net.corda.node.internal.Node import net.corda.node.serialization.NodeClock @@ -21,7 +20,7 @@ import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.nodeapi.User import net.corda.nodeapi.config.parseAs -import net.corda.testing.MOCK_VERSION_INFO +import net.corda.testing.DUMMY_MAP import net.corda.testing.driver.addressMustNotBeBoundFuture import net.corda.testing.getFreeLocalPorts import org.apache.logging.log4j.Level diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt b/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt index 6731e35c5c..2527ae8e0b 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt @@ -19,7 +19,6 @@ import net.corda.node.services.network.InMemoryNetworkMapCache import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction -import net.corda.testing.MOCK_VERSION_INFO import net.corda.testing.freeLocalHostAndPort import org.jetbrains.exposed.sql.Database import java.io.Closeable From 68068e5640cf6c00ec452b2ed95bc3b4006b2f3f Mon Sep 17 00:00:00 2001 From: Clinton Alexander Date: Thu, 6 Jul 2017 15:14:07 +0100 Subject: [PATCH 65/97] Cordapps now exclude the META-INF of dependencies. --- constants.properties | 2 +- .../src/main/groovy/net/corda/plugins/Cordformation.groovy | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/constants.properties b/constants.properties index eebbf86a14..6993620217 100644 --- a/constants.properties +++ b/constants.properties @@ -1,4 +1,4 @@ -gradlePluginsVersion=0.13.1 +gradlePluginsVersion=0.13.2 kotlinVersion=1.1.1 guavaVersion=21.0 bouncycastleVersion=1.57 diff --git a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy index 6d41b9d370..413f05faa5 100644 --- a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy +++ b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy @@ -24,7 +24,9 @@ class Cordformation implements Plugin { // Note: project.afterEvaluate did not have full dependency resolution completed, hence a task is used instead def task = project.task('configureCordappFatJar') { doLast { - project.tasks.jar.from getDirectNonCordaDependencies(project).collect { project.zipTree(it) }.flatten() + project.tasks.jar.from getDirectNonCordaDependencies(project).collect { + project.zipTree(it).matching { exclude { it.path.contains('META-INF') } } + }.flatten() } } project.tasks.jar.dependsOn task From 2b3f6d970147b12fdb7e2e4c94af7eee4bf6c798 Mon Sep 17 00:00:00 2001 From: Clinton Alexander Date: Thu, 6 Jul 2017 15:15:47 +0100 Subject: [PATCH 66/97] Revert "Cordapps now exclude the META-INF of dependencies." This reverts commit 68068e5640cf6c00ec452b2ed95bc3b4006b2f3f. --- constants.properties | 2 +- .../src/main/groovy/net/corda/plugins/Cordformation.groovy | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/constants.properties b/constants.properties index 6993620217..eebbf86a14 100644 --- a/constants.properties +++ b/constants.properties @@ -1,4 +1,4 @@ -gradlePluginsVersion=0.13.2 +gradlePluginsVersion=0.13.1 kotlinVersion=1.1.1 guavaVersion=21.0 bouncycastleVersion=1.57 diff --git a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy index 413f05faa5..6d41b9d370 100644 --- a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy +++ b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy @@ -24,9 +24,7 @@ class Cordformation implements Plugin { // Note: project.afterEvaluate did not have full dependency resolution completed, hence a task is used instead def task = project.task('configureCordappFatJar') { doLast { - project.tasks.jar.from getDirectNonCordaDependencies(project).collect { - project.zipTree(it).matching { exclude { it.path.contains('META-INF') } } - }.flatten() + project.tasks.jar.from getDirectNonCordaDependencies(project).collect { project.zipTree(it) }.flatten() } } project.tasks.jar.dependsOn task From fc97fb2368aa0457e38a85c74d6a35d11b63b35c Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Thu, 6 Jul 2017 15:06:04 +0100 Subject: [PATCH 67/97] Removed DeserializeAsKotlinObjectDef interface as serialisation of Kotlin objects is now handled automatically --- .../corda/core/contracts/TransactionTypes.kt | 5 ++--- .../serialization/DefaultKryoCustomizer.kt | 3 --- .../net/corda/core/serialization/Kryo.kt | 16 +--------------- .../net/corda/core/serialization/KryoTests.kt | 18 ++++++++++-------- .../corda/node/services/messaging/Messaging.kt | 8 -------- .../statemachine/StateMachineManager.kt | 1 + 6 files changed, 14 insertions(+), 37 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/contracts/TransactionTypes.kt b/core/src/main/kotlin/net/corda/core/contracts/TransactionTypes.kt index 5a2fbdb917..db388b5c8c 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TransactionTypes.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TransactionTypes.kt @@ -2,7 +2,6 @@ package net.corda.core.contracts import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.DeserializeAsKotlinObjectDef import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder import java.security.PublicKey @@ -61,7 +60,7 @@ sealed class TransactionType { abstract fun verifyTransaction(tx: LedgerTransaction) /** A general transaction type where transaction validity is determined by custom contract code */ - object General : TransactionType(), DeserializeAsKotlinObjectDef { + object General : TransactionType() { /** Just uses the default [TransactionBuilder] with no special logic */ class Builder(notary: Party?) : TransactionBuilder(General, notary) @@ -141,7 +140,7 @@ sealed class TransactionType { * A special transaction type for reassigning a notary for a state. Validation does not involve running * any contract code, it just checks that the states are unmodified apart from the notary field. */ - object NotaryChange : TransactionType(), DeserializeAsKotlinObjectDef { + object NotaryChange : TransactionType() { /** * A transaction builder that automatically sets the transaction type to [NotaryChange] * and adds the list of participants to the signers set for every input state. diff --git a/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt b/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt index edf3792bea..f32d1e9d2b 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt @@ -86,9 +86,6 @@ object DefaultKryoCustomizer { // This ensures a NonEmptySetSerializer is constructed with an initial value. register(NonEmptySet::class.java, NonEmptySetSerializer) - /** This ensures any kotlin objects that implement [DeserializeAsKotlinObjectDef] are read back in as singletons. */ - addDefaultSerializer(DeserializeAsKotlinObjectDef::class.java, KotlinObjectSerializer) - addDefaultSerializer(SerializeAsToken::class.java, SerializeAsTokenSerializer()) register(MetaData::class.java, MetaDataSerializer) diff --git a/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt b/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt index de0dea59cb..b6757d5cf4 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt @@ -462,20 +462,6 @@ inline fun readListOfLength(kryo: Kryo, input: Input, minLen: Int = return list } -/** Marker interface for kotlin object definitions so that they are deserialized as the singleton instance. */ -// TODO This is not needed anymore -interface DeserializeAsKotlinObjectDef - -/** Serializer to deserialize kotlin object definitions marked with [DeserializeAsKotlinObjectDef]. */ -object KotlinObjectSerializer : Serializer() { - override fun read(kryo: Kryo, input: Input, type: Class): DeserializeAsKotlinObjectDef { - // read the public static INSTANCE field that kotlin compiler generates. - return type.getField("INSTANCE").get(null) as DeserializeAsKotlinObjectDef - } - - override fun write(kryo: Kryo, output: Output, obj: DeserializeAsKotlinObjectDef) {} -} - // No ClassResolver only constructor. MapReferenceResolver is the default as used by Kryo in other constructors. private val internalKryoPool = KryoPool.Builder { DefaultKryoCustomizer.customize(CordaKryo(makeAllButBlacklistedClassResolver())) }.build() private val kryoPool = KryoPool.Builder { DefaultKryoCustomizer.customize(CordaKryo(makeStandardClassResolver())) }.build() @@ -533,7 +519,7 @@ inline fun Kryo.register( return register( type.java, object : Serializer() { - override fun read(kryo: Kryo, input: Input, type: Class): T = read(kryo, input) + override fun read(kryo: Kryo, input: Input, clazz: Class): T = read(kryo, input) override fun write(kryo: Kryo, output: Output, obj: T) = write(kryo, output, obj) } ) diff --git a/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt b/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt index daa27fe0cb..1b70f24b68 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt @@ -3,10 +3,9 @@ package net.corda.core.serialization import com.esotericsoftware.kryo.Kryo import com.google.common.primitives.Ints import net.corda.core.crypto.* +import net.corda.node.services.persistence.NodeAttachmentService import net.corda.testing.ALICE import net.corda.testing.BOB -import net.corda.node.services.messaging.Ack -import net.corda.node.services.persistence.NodeAttachmentService import net.corda.testing.BOB_PUBKEY import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy @@ -16,7 +15,8 @@ import org.junit.Test import org.slf4j.LoggerFactory import java.io.ByteArrayInputStream import java.io.InputStream -import java.security.cert.* +import java.security.cert.CertPath +import java.security.cert.CertificateFactory import java.time.Instant import java.util.* import kotlin.test.assertEquals @@ -92,11 +92,10 @@ class KryoTests { } @Test - fun `write and read Ack`() { - val tokenizableBefore = Ack - val serializedBytes = tokenizableBefore.serialize(kryo) - val tokenizableAfter = serializedBytes.deserialize(kryo) - assertThat(tokenizableAfter).isSameAs(tokenizableBefore) + fun `write and read Kotlin object singleton`() { + val serialised = TestSingleton.serialize(kryo) + val deserialised = serialised.deserialize(kryo) + assertThat(deserialised).isSameAs(TestSingleton) } @Test @@ -173,4 +172,7 @@ class KryoTests { override fun toString(): String = "Cyclic($value)" } + @CordaSerializable + private object TestSingleton + } diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/Messaging.kt b/node/src/main/kotlin/net/corda/node/services/messaging/Messaging.kt index d9003681d9..c5396a0067 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/Messaging.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/Messaging.kt @@ -8,7 +8,6 @@ import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.services.DEFAULT_SESSION_ID import net.corda.core.node.services.PartyInfo import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.DeserializeAsKotlinObjectDef import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import org.bouncycastle.asn1.x500.X500Name @@ -229,10 +228,3 @@ object TopicStringValidator { /** @throws IllegalArgumentException if the given topic contains invalid characters */ fun check(tag: String) = require(regex.matcher(tag).matches()) } - -/** - * A general Ack message that conveys no content other than it's presence for use when you want an acknowledgement - * from a recipient. Using [Unit] can be ambiguous as it is similar to [Void] and so could mean no response. - */ -@CordaSerializable -object Ack : DeserializeAsKotlinObjectDef diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt index 8066a7293f..3e1a8bdf35 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt @@ -89,6 +89,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, } }.build() + // TODO Move this into the blacklist and upgrade the blacklist to allow custom messages private object AutoCloseableSerialisationDetector : Serializer() { override fun write(kryo: Kryo, output: Output, closeable: AutoCloseable) { val message = if (closeable is CloseableIterator<*>) { From c1cd7d6b799fd37faf2d69ab2490647ed10044d7 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Thu, 6 Jul 2017 17:03:29 +0100 Subject: [PATCH 68/97] Take out blanket import of Character TYPE --- .../corda/core/serialization/carpenter/ClassCarpenter.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/serialization/carpenter/ClassCarpenter.kt b/core/src/main/kotlin/net/corda/core/serialization/carpenter/ClassCarpenter.kt index b433cb417a..784cc5d0f5 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/carpenter/ClassCarpenter.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/carpenter/ClassCarpenter.kt @@ -4,7 +4,10 @@ import org.objectweb.asm.ClassWriter import org.objectweb.asm.MethodVisitor import org.objectweb.asm.Opcodes.* import org.objectweb.asm.Type -import java.lang.Character.* + +import java.lang.Character.isJavaIdentifierPart +import java.lang.Character.isJavaIdentifierStart + import java.util.* /** @@ -333,7 +336,7 @@ class ClassCarpenter { visitFieldInsn(GETFIELD, schema.jvmName, name, type.descriptor) when (type.field) { java.lang.Boolean.TYPE, Integer.TYPE, java.lang.Short.TYPE, java.lang.Byte.TYPE, - java.lang.Character.TYPE, TYPE -> visitInsn(IRETURN) + java.lang.Character.TYPE -> visitInsn(IRETURN) java.lang.Long.TYPE -> visitInsn(LRETURN) java.lang.Double.TYPE -> visitInsn(DRETURN) java.lang.Float.TYPE -> visitInsn(FRETURN) @@ -399,7 +402,7 @@ class ClassCarpenter { private fun MethodVisitor.load(slot: Int, type: Field): Int { when (type.field) { java.lang.Boolean.TYPE, Integer.TYPE, java.lang.Short.TYPE, java.lang.Byte.TYPE, - java.lang.Character.TYPE, TYPE -> visitVarInsn(ILOAD, slot) + java.lang.Character.TYPE -> visitVarInsn(ILOAD, slot) java.lang.Long.TYPE -> visitVarInsn(LLOAD, slot) java.lang.Double.TYPE -> visitVarInsn(DLOAD, slot) java.lang.Float.TYPE -> visitVarInsn(FLOAD, slot) From 8f1529b863554157800a79e0b04b817150b30dbd Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Thu, 6 Jul 2017 17:58:18 +0100 Subject: [PATCH 69/97] Moved ByteArrays.kt to core.utilities --- .../kotlin/net/corda/jackson/JacksonSupport.kt | 2 +- .../net/corda/client/jfx/NodeMonitorModelTest.kt | 2 +- .../kotlin/net/corda/client/mock/EventGenerator.kt | 2 +- .../kotlin/net/corda/client/mock/Generators.kt | 2 +- .../net/corda/client/rpc/CordaRPCClientTest.kt | 2 +- .../kotlin/rpc/StandaloneCordaRPClientTest.kt | 2 +- .../kotlin/net/corda/core/contracts/Structures.kt | 1 + .../kotlin/net/corda/core/crypto/CryptoUtils.kt | 2 +- .../net/corda/core/crypto/DigitalSignature.kt | 2 +- .../main/kotlin/net/corda/core/crypto/MetaData.kt | 2 +- .../kotlin/net/corda/core/crypto/SecureHash.kt | 2 +- .../net/corda/core/identity/AbstractParty.kt | 2 +- .../net/corda/core/identity/AnonymousParty.kt | 2 +- .../main/kotlin/net/corda/core/identity/Party.kt | 4 +--- .../net/corda/core/node/services/Services.kt | 2 +- .../core/node/services/vault/QueryCriteria.kt | 2 +- .../net/corda/core/schemas/PersistentTypes.kt | 2 +- .../kotlin/net/corda/core/serialization/Kryo.kt | 1 + .../net/corda/core/serialization/amqp/Schema.kt | 2 +- .../{serialization => utilities}/ByteArrays.kt | 14 +++++++++----- .../net/corda/core/crypto/CompositeKeyTests.kt | 2 +- .../corda/core/flows/ContractUpgradeFlowTest.kt | 2 +- .../core/flows/ResolveTransactionsFlowTest.kt | 2 +- .../net/corda/core/serialization/KryoTests.kt | 1 + .../core/serialization/SerializationTokenTest.kt | 1 + .../kotlin/net/corda/core/testing/Generators.kt | 2 +- .../net/corda/docs/IntegrationTestingTutorial.kt | 2 +- .../kotlin/net/corda/docs/ClientRpcTutorial.kt | 2 +- .../corda/docs/FxTransactionBuildTutorialTest.kt | 2 +- .../main/kotlin/net/corda/flows/CashExitFlow.kt | 2 +- .../main/kotlin/net/corda/flows/CashFlowCommand.kt | 2 +- .../main/kotlin/net/corda/flows/CashIssueFlow.kt | 2 +- .../src/main/kotlin/net/corda/flows/IssuerFlow.kt | 2 +- .../net/corda/contracts/asset/CashTestsJava.java | 2 +- .../kotlin/net/corda/contracts/asset/CashTests.kt | 2 +- .../net/corda/contracts/asset/ObligationTests.kt | 2 +- .../kotlin/net/corda/flows/CashExitFlowTests.kt | 2 +- .../kotlin/net/corda/flows/CashIssueFlowTests.kt | 2 +- .../kotlin/net/corda/flows/CashPaymentFlowTests.kt | 2 +- .../test/kotlin/net/corda/flows/IssuerFlowTest.kt | 2 +- .../kotlin/net/corda/node/NodePerformanceTests.kt | 2 +- .../corda/node/services/DistributedServiceTests.kt | 2 +- .../node/services/messaging/NodeMessagingClient.kt | 2 +- .../services/vault/HibernateQueryCriteriaParser.kt | 4 ++-- .../corda/node/services/vault/NodeVaultService.kt | 2 ++ .../net/corda/node/services/vault/VaultSchema.kt | 4 +--- .../node/services/vault/VaultQueryJavaTests.java | 2 +- .../kotlin/net/corda/node/CordaRPCOpsImplTest.kt | 2 +- .../services/statemachine/FlowFrameworkTests.kt | 2 +- .../node/services/vault/NodeVaultServiceTest.kt | 2 +- .../corda/node/services/vault/VaultQueryTests.kt | 2 +- .../net/corda/bank/api/BankOfCordaClientApi.kt | 2 +- .../kotlin/net/corda/bank/api/BankOfCordaWebApi.kt | 2 +- .../net/corda/traderdemo/TraderDemoClientApi.kt | 2 +- .../main/kotlin/net/corda/testing/CoreTestUtils.kt | 2 +- .../net/corda/testing/contracts/VaultFiller.kt | 2 +- .../net/corda/explorer/ExplorerSimulation.kt | 2 +- .../explorer/views/cordapps/cash/NewTransaction.kt | 2 +- .../net/corda/loadtest/tests/CrossCashTest.kt | 2 +- .../net/corda/loadtest/tests/GenerateHelpers.kt | 2 +- .../net/corda/loadtest/tests/StabilityTest.kt | 2 +- .../kotlin/net/corda/verifier/VerifierTests.kt | 2 +- 62 files changed, 72 insertions(+), 66 deletions(-) rename core/src/main/kotlin/net/corda/core/{serialization => utilities}/ByteArrays.kt (90%) diff --git a/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt b/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt index eaaeff5306..0d72c0d3d5 100644 --- a/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt +++ b/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt @@ -16,7 +16,7 @@ import net.corda.core.identity.Party import net.corda.core.messaging.CordaRPCOps import net.corda.core.node.NodeInfo import net.corda.core.node.services.IdentityService -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.i2p.crypto.eddsa.EdDSAPublicKey diff --git a/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt b/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt index 94436ea72b..5c93d9e820 100644 --- a/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt +++ b/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt @@ -19,7 +19,7 @@ import net.corda.core.node.NodeInfo import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.Vault -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.transactions.SignedTransaction import net.corda.testing.ALICE import net.corda.testing.BOB diff --git a/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt b/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt index 7c4503fb36..6bd7b68e7c 100644 --- a/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt +++ b/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt @@ -4,7 +4,7 @@ import net.corda.core.contracts.Amount import net.corda.core.contracts.GBP import net.corda.core.contracts.USD import net.corda.core.identity.Party -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.flows.CashFlowCommand import java.util.* diff --git a/client/mock/src/main/kotlin/net/corda/client/mock/Generators.kt b/client/mock/src/main/kotlin/net/corda/client/mock/Generators.kt index dd766cc4c9..7b35b8d5f8 100644 --- a/client/mock/src/main/kotlin/net/corda/client/mock/Generators.kt +++ b/client/mock/src/main/kotlin/net/corda/client/mock/Generators.kt @@ -1,7 +1,7 @@ package net.corda.client.mock import net.corda.core.contracts.Amount -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import java.util.* fun generateCurrency(): Generator { diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt index 944bc37b2e..f7062286ab 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt @@ -6,7 +6,7 @@ import net.corda.core.getOrThrow import net.corda.core.messaging.* import net.corda.core.node.services.ServiceInfo import net.corda.core.random63BitValue -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.testing.ALICE import net.corda.flows.CashException import net.corda.flows.CashIssueFlow diff --git a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt index eab25e0eb6..a5a6002c50 100644 --- a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt +++ b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt @@ -18,7 +18,7 @@ import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.Sort import net.corda.core.node.services.vault.SortAttribute import net.corda.core.seconds -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.sizedInputStreamAndHash import net.corda.core.utilities.loggerFor import net.corda.flows.CashIssueFlow diff --git a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt index abfb9bad05..0740a4a7e4 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt @@ -7,6 +7,7 @@ import net.corda.core.flows.FlowLogicRefFactory import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.serialization.* +import net.corda.core.utilities.OpaqueBytes import java.io.FileNotFoundException import java.io.IOException import java.io.InputStream diff --git a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt index 2094a526c7..0f0b73ea0a 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt @@ -3,7 +3,7 @@ package net.corda.core.crypto import net.corda.core.identity.Party -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import java.math.BigInteger import java.security.* diff --git a/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt b/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt index 01c0a0d2be..738f1e108b 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt @@ -2,7 +2,7 @@ package net.corda.core.crypto import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import java.security.InvalidKeyException import java.security.PublicKey import java.security.SignatureException diff --git a/core/src/main/kotlin/net/corda/core/crypto/MetaData.kt b/core/src/main/kotlin/net/corda/core/crypto/MetaData.kt index a8dc49ae8e..edcf018e82 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/MetaData.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/MetaData.kt @@ -1,7 +1,7 @@ package net.corda.core.crypto import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.opaque +import net.corda.core.utilities.opaque import net.corda.core.serialization.serialize import java.security.PublicKey import java.time.Instant diff --git a/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt b/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt index ee8f4a5afe..fcefe20a5b 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt @@ -2,7 +2,7 @@ package net.corda.core.crypto import com.google.common.io.BaseEncoding import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import java.security.MessageDigest /** diff --git a/core/src/main/kotlin/net/corda/core/identity/AbstractParty.kt b/core/src/main/kotlin/net/corda/core/identity/AbstractParty.kt index 5c81e0c4b2..7dc89ae4a5 100644 --- a/core/src/main/kotlin/net/corda/core/identity/AbstractParty.kt +++ b/core/src/main/kotlin/net/corda/core/identity/AbstractParty.kt @@ -2,7 +2,7 @@ package net.corda.core.identity import net.corda.core.contracts.PartyAndReference import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import org.bouncycastle.asn1.x500.X500Name import java.security.PublicKey diff --git a/core/src/main/kotlin/net/corda/core/identity/AnonymousParty.kt b/core/src/main/kotlin/net/corda/core/identity/AnonymousParty.kt index dc1ec16f58..33ffffb19b 100644 --- a/core/src/main/kotlin/net/corda/core/identity/AnonymousParty.kt +++ b/core/src/main/kotlin/net/corda/core/identity/AnonymousParty.kt @@ -2,7 +2,7 @@ package net.corda.core.identity import net.corda.core.contracts.PartyAndReference import net.corda.core.crypto.toBase58String -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import org.bouncycastle.asn1.x500.X500Name import java.security.PublicKey diff --git a/core/src/main/kotlin/net/corda/core/identity/Party.kt b/core/src/main/kotlin/net/corda/core/identity/Party.kt index 7bc6ebab8b..e41c550c84 100644 --- a/core/src/main/kotlin/net/corda/core/identity/Party.kt +++ b/core/src/main/kotlin/net/corda/core/identity/Party.kt @@ -2,9 +2,7 @@ package net.corda.core.identity import net.corda.core.contracts.PartyAndReference import net.corda.core.crypto.CertificateAndKeyPair -import net.corda.core.crypto.toBase58String -import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import org.bouncycastle.asn1.x500.X500Name import java.security.PublicKey diff --git a/core/src/main/kotlin/net/corda/core/node/services/Services.kt b/core/src/main/kotlin/net/corda/core/node/services/Services.kt index d119662533..bdc8f00d0f 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/Services.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/Services.kt @@ -16,7 +16,7 @@ import net.corda.core.node.services.vault.PageSpecification import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.Sort import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.toFuture import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt index fc3f0cad0d..0bbce100e5 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt @@ -9,7 +9,7 @@ import net.corda.core.identity.AbstractParty import net.corda.core.node.services.Vault import net.corda.core.schemas.PersistentState import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import org.bouncycastle.asn1.x500.X500Name import java.time.Instant import java.util.* diff --git a/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt b/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt index b615e75b64..36a847eec3 100644 --- a/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt +++ b/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt @@ -3,7 +3,7 @@ package net.corda.core.schemas import io.requery.Persistable import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateRef -import net.corda.core.serialization.toHexString +import net.corda.core.utilities.toHexString import java.io.Serializable import javax.persistence.Column import javax.persistence.Embeddable diff --git a/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt b/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt index b6757d5cf4..5b043cd33b 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt @@ -13,6 +13,7 @@ import net.corda.core.identity.Party import net.corda.core.node.AttachmentsClassLoader import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.LazyPool +import net.corda.core.utilities.OpaqueBytes import net.i2p.crypto.eddsa.EdDSAPrivateKey import net.i2p.crypto.eddsa.EdDSAPublicKey import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec 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 507c2f0a6f..844f7ce51b 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 @@ -3,7 +3,7 @@ package net.corda.core.serialization.amqp import com.google.common.hash.Hasher import com.google.common.hash.Hashing import net.corda.core.crypto.toBase64 -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import org.apache.qpid.proton.amqp.DescribedType import org.apache.qpid.proton.amqp.UnsignedLong import org.apache.qpid.proton.codec.Data diff --git a/core/src/main/kotlin/net/corda/core/serialization/ByteArrays.kt b/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt similarity index 90% rename from core/src/main/kotlin/net/corda/core/serialization/ByteArrays.kt rename to core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt index 9c36c9d19b..3102086b43 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/ByteArrays.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt @@ -1,6 +1,9 @@ -package net.corda.core.serialization +@file:JvmName("ByteArrays") + +package net.corda.core.utilities import com.google.common.io.BaseEncoding +import net.corda.core.serialization.CordaSerializable import java.io.ByteArrayInputStream import java.util.* @@ -11,12 +14,13 @@ import java.util.* */ @CordaSerializable open class OpaqueBytes(val bytes: ByteArray) { - init { - check(bytes.isNotEmpty()) + companion object { + @JvmStatic + fun of(vararg b: Byte) = OpaqueBytes(byteArrayOf(*b)) } - companion object { - fun of(vararg b: Byte) = OpaqueBytes(byteArrayOf(*b)) + init { + check(bytes.isNotEmpty()) } override fun equals(other: Any?): Boolean { diff --git a/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt b/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt index 9029da4448..591d56001a 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt @@ -1,6 +1,6 @@ package net.corda.core.crypto -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.serialization.serialize import org.junit.Test import kotlin.test.assertEquals diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt index d8661ac41c..3f8cfae6ae 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -12,7 +12,7 @@ import net.corda.core.identity.Party import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.startFlow import net.corda.core.node.services.unconsumedStates -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.Emoji import net.corda.flows.CashIssueFlow diff --git a/core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt index 87d61276de..2336298996 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt @@ -4,7 +4,7 @@ import net.corda.core.contracts.testing.DummyContract import net.corda.core.crypto.SecureHash import net.corda.core.getOrThrow import net.corda.core.identity.Party -import net.corda.core.serialization.opaque +import net.corda.core.utilities.opaque import net.corda.core.transactions.SignedTransaction import net.corda.testing.DUMMY_NOTARY_KEY import net.corda.flows.ResolveTransactionsFlow diff --git a/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt b/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt index 1b70f24b68..e27932c757 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt @@ -3,6 +3,7 @@ package net.corda.core.serialization import com.esotericsoftware.kryo.Kryo import com.google.common.primitives.Ints import net.corda.core.crypto.* +import net.corda.core.utilities.opaque import net.corda.node.services.persistence.NodeAttachmentService import net.corda.testing.ALICE import net.corda.testing.BOB diff --git a/core/src/test/kotlin/net/corda/core/serialization/SerializationTokenTest.kt b/core/src/test/kotlin/net/corda/core/serialization/SerializationTokenTest.kt index 450fbf1d18..9b2517c8d5 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/SerializationTokenTest.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/SerializationTokenTest.kt @@ -5,6 +5,7 @@ import com.esotericsoftware.kryo.KryoException import com.esotericsoftware.kryo.io.Output import com.nhaarman.mockito_kotlin.mock import net.corda.core.node.ServiceHub +import net.corda.core.utilities.OpaqueBytes import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Before diff --git a/core/src/test/kotlin/net/corda/core/testing/Generators.kt b/core/src/test/kotlin/net/corda/core/testing/Generators.kt index bf08440e51..591dbb13c1 100644 --- a/core/src/test/kotlin/net/corda/core/testing/Generators.kt +++ b/core/src/test/kotlin/net/corda/core/testing/Generators.kt @@ -9,7 +9,7 @@ import net.corda.core.crypto.SecureHash import net.corda.core.crypto.entropyToKeyPair import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.testing.getTestX509Name import org.bouncycastle.asn1.x500.X500Name import java.nio.ByteBuffer diff --git a/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt index 51ca156e6d..c51e115f09 100644 --- a/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt +++ b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt @@ -8,7 +8,7 @@ import net.corda.core.getOrThrow import net.corda.core.messaging.startFlow import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.Vault -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.testing.ALICE import net.corda.testing.BOB import net.corda.testing.DUMMY_NOTARY diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt index 0581e63abd..ee176d7847 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt @@ -9,7 +9,7 @@ import net.corda.core.messaging.startFlow import net.corda.core.node.CordaPluginRegistry import net.corda.core.node.services.ServiceInfo import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.serialization.SerializationCustomization import net.corda.core.transactions.SignedTransaction import net.corda.testing.ALICE diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt index 178d4c9b9e..be1b5fa83f 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt @@ -3,7 +3,7 @@ package net.corda.docs import net.corda.core.contracts.* import net.corda.core.getOrThrow import net.corda.core.node.services.ServiceInfo -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.toFuture import net.corda.testing.DUMMY_NOTARY import net.corda.testing.DUMMY_NOTARY_KEY diff --git a/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt b/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt index a80bf0ea8e..9a363fffc2 100644 --- a/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt @@ -8,7 +8,7 @@ import net.corda.core.contracts.TransactionType import net.corda.core.contracts.issuedBy import net.corda.core.flows.StartableByRPC import net.corda.core.identity.Party -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker import java.util.* diff --git a/finance/src/main/kotlin/net/corda/flows/CashFlowCommand.kt b/finance/src/main/kotlin/net/corda/flows/CashFlowCommand.kt index 30012b6b9e..446cca5b4d 100644 --- a/finance/src/main/kotlin/net/corda/flows/CashFlowCommand.kt +++ b/finance/src/main/kotlin/net/corda/flows/CashFlowCommand.kt @@ -5,7 +5,7 @@ import net.corda.core.identity.Party import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.FlowHandle import net.corda.core.messaging.startFlow -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import java.util.* /** diff --git a/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt b/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt index 3147960163..ec9659f1d5 100644 --- a/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt @@ -7,7 +7,7 @@ import net.corda.core.contracts.TransactionType import net.corda.core.contracts.issuedBy import net.corda.core.flows.StartableByRPC import net.corda.core.identity.Party -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker import java.util.* diff --git a/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt b/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt index 25fec82ee8..3b4771a652 100644 --- a/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt @@ -6,7 +6,7 @@ import net.corda.core.contracts.* import net.corda.core.flows.* import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.unwrap diff --git a/finance/src/test/java/net/corda/contracts/asset/CashTestsJava.java b/finance/src/test/java/net/corda/contracts/asset/CashTestsJava.java index 608df6a980..ed091c6104 100644 --- a/finance/src/test/java/net/corda/contracts/asset/CashTestsJava.java +++ b/finance/src/test/java/net/corda/contracts/asset/CashTestsJava.java @@ -3,7 +3,7 @@ package net.corda.contracts.asset; import kotlin.Unit; import net.corda.core.contracts.PartyAndReference; import net.corda.core.identity.AnonymousParty; -import net.corda.core.serialization.OpaqueBytes; +import net.corda.core.utilities.OpaqueBytes; import org.junit.Test; import static net.corda.core.contracts.ContractsDSL.DOLLARS; diff --git a/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt b/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt index 8477a66428..c27a2b4856 100644 --- a/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt @@ -10,7 +10,7 @@ import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.node.services.VaultService import net.corda.core.node.services.unconsumedStates -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.* diff --git a/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt b/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt index 541b9b9997..78f2a65759 100644 --- a/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt @@ -10,7 +10,7 @@ import net.corda.core.hours import net.corda.core.crypto.testing.NULL_PARTY import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.* import net.corda.testing.* import net.corda.testing.node.MockServices diff --git a/finance/src/test/kotlin/net/corda/flows/CashExitFlowTests.kt b/finance/src/test/kotlin/net/corda/flows/CashExitFlowTests.kt index bb9231fe42..b5ff01bff8 100644 --- a/finance/src/test/kotlin/net/corda/flows/CashExitFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/flows/CashExitFlowTests.kt @@ -5,7 +5,7 @@ import net.corda.core.contracts.DOLLARS import net.corda.core.contracts.`issued by` import net.corda.core.getOrThrow import net.corda.core.identity.Party -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork.MockNode diff --git a/finance/src/test/kotlin/net/corda/flows/CashIssueFlowTests.kt b/finance/src/test/kotlin/net/corda/flows/CashIssueFlowTests.kt index 78abdb4bf0..db183cabff 100644 --- a/finance/src/test/kotlin/net/corda/flows/CashIssueFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/flows/CashIssueFlowTests.kt @@ -5,7 +5,7 @@ import net.corda.core.contracts.DOLLARS import net.corda.core.contracts.`issued by` import net.corda.core.getOrThrow import net.corda.core.identity.Party -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork.MockNode diff --git a/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt b/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt index 0cc5c91dc1..5e04b65b67 100644 --- a/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt @@ -5,7 +5,7 @@ import net.corda.core.contracts.DOLLARS import net.corda.core.contracts.`issued by` import net.corda.core.getOrThrow import net.corda.core.identity.Party -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork.MockNode diff --git a/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt b/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt index 607da6fbfb..97156d6a6d 100644 --- a/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt +++ b/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt @@ -10,7 +10,7 @@ import net.corda.core.internal.FlowStateMachine import net.corda.core.getOrThrow import net.corda.core.identity.Party import net.corda.core.map -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.toFuture import net.corda.core.transactions.SignedTransaction import net.corda.testing.DUMMY_NOTARY diff --git a/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt b/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt index 3b42fbeae0..c22c69ec34 100644 --- a/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt @@ -9,7 +9,7 @@ import net.corda.core.flows.StartableByRPC import net.corda.core.messaging.startFlow import net.corda.core.minutes import net.corda.core.node.services.ServiceInfo -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.div import net.corda.flows.CashIssueFlow import net.corda.flows.CashPaymentFlow diff --git a/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt index 219d730901..efb4bf8b80 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt @@ -9,7 +9,7 @@ import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.StateMachineUpdate import net.corda.core.messaging.startFlow import net.corda.core.node.NodeInfo -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.testing.ALICE import net.corda.testing.DUMMY_NOTARY import net.corda.flows.CashIssueFlow diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt index 56afb69dd3..0f73b693ea 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt @@ -9,7 +9,7 @@ import net.corda.core.messaging.RPCOps import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.services.PartyInfo import net.corda.core.node.services.TransactionVerifierService -import net.corda.core.serialization.opaque +import net.corda.core.utilities.opaque import net.corda.core.transactions.LedgerTransaction import net.corda.core.utilities.loggerFor import net.corda.core.utilities.trace diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt index f0456d66c2..eef63d1d68 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt @@ -10,8 +10,8 @@ import net.corda.core.node.services.vault.* import net.corda.core.node.services.vault.QueryCriteria.CommonQueryCriteria import net.corda.core.schemas.PersistentState import net.corda.core.schemas.PersistentStateRef -import net.corda.core.serialization.OpaqueBytes -import net.corda.core.serialization.toHexString +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.toHexString import net.corda.core.utilities.loggerFor import net.corda.core.utilities.trace import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1 diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index 159f784bdb..597e9f2595 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -26,7 +26,9 @@ import net.corda.core.serialization.* import net.corda.core.tee import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction +import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.toHexString import net.corda.core.utilities.trace import net.corda.node.services.database.RequeryConfiguration import net.corda.node.services.statemachine.FlowStateMachineImpl diff --git a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt index 24cd7baf0f..316ea64e03 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt @@ -2,12 +2,10 @@ package net.corda.node.services.vault.schemas.jpa import net.corda.core.contracts.UniqueIdentifier import net.corda.core.identity.AbstractParty -import net.corda.core.identity.AnonymousParty import net.corda.core.node.services.Vault import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState -import net.corda.core.serialization.OpaqueBytes -import java.security.PublicKey +import net.corda.core.utilities.OpaqueBytes import java.time.Instant import java.util.* import javax.persistence.* diff --git a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java index 8e68e28aa6..e2ffa4359d 100644 --- a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java +++ b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java @@ -19,7 +19,7 @@ import net.corda.core.node.services.vault.QueryCriteria.VaultCustomQueryCriteria import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria; import net.corda.core.schemas.MappedSchema; import net.corda.core.schemas.testing.DummyLinearStateSchemaV1; -import net.corda.core.serialization.OpaqueBytes; +import net.corda.core.utilities.OpaqueBytes; import net.corda.core.transactions.SignedTransaction; import net.corda.core.transactions.WireTransaction; import net.corda.node.services.database.HibernateConfiguration; diff --git a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt index d592019815..11009e3c42 100644 --- a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt @@ -12,7 +12,7 @@ import net.corda.core.messaging.* import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.Vault import net.corda.core.node.services.unconsumedStates -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.transactions.SignedTransaction import net.corda.flows.CashIssueFlow import net.corda.flows.CashPaymentFlow diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt index 1ed12b9fd3..9773a7f733 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt @@ -21,7 +21,7 @@ import net.corda.core.node.services.PartyInfo import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.queryBy import net.corda.core.node.services.unconsumedStates -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.serialization.deserialize import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder diff --git a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt index 23feae2385..8ce32149e0 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt @@ -8,7 +8,7 @@ import net.corda.core.identity.AnonymousParty import net.corda.core.node.services.StatesNotAvailableException import net.corda.core.node.services.VaultService import net.corda.core.node.services.unconsumedStates -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.transactions.SignedTransaction import net.corda.testing.DUMMY_NOTARY import net.corda.core.utilities.LogHelper diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index ecdb09843b..1d2fbd7cbf 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -16,7 +16,7 @@ import net.corda.core.node.services.vault.* import net.corda.core.node.services.vault.QueryCriteria.* import net.corda.core.schemas.testing.DummyLinearStateSchemaV1 import net.corda.core.seconds -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.transactions.SignedTransaction import net.corda.node.services.database.HibernateConfiguration import net.corda.node.services.schema.NodeSchemaService diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt index 0aa1647925..7c5563d323 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt @@ -7,7 +7,7 @@ import net.corda.core.contracts.Amount import net.corda.core.contracts.currency import net.corda.core.getOrThrow import net.corda.core.messaging.startFlow -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.transactions.SignedTransaction import net.corda.flows.IssuerFlow.IssuanceRequester import net.corda.testing.http.HttpApi diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt index 4e2e515a52..1b74f2e0d2 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt @@ -6,7 +6,7 @@ import net.corda.core.flows.FlowException import net.corda.core.getOrThrow import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.startFlow -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.loggerFor import net.corda.flows.IssuerFlow.IssuanceRequester import org.bouncycastle.asn1.x500.X500Name diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt index 68bb39825d..434f652257 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt @@ -12,7 +12,7 @@ import net.corda.core.contracts.filterStatesOfType import net.corda.core.getOrThrow import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.startFlow -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.Emoji import net.corda.core.utilities.loggerFor import net.corda.flows.IssuerFlow.IssuanceRequester diff --git a/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt b/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt index 688b3e2bb8..bffece9c57 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt @@ -12,7 +12,7 @@ import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.ServiceHub import net.corda.core.node.services.IdentityService -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.transactions.TransactionBuilder import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.VerifierType diff --git a/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt b/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt index 7e5382f1e8..9ee4f513c2 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt @@ -13,7 +13,7 @@ import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.node.ServiceHub import net.corda.core.node.services.Vault -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.transactions.SignedTransaction import net.corda.testing.CHARLIE import net.corda.testing.DUMMY_NOTARY diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt index 329086e2c1..bbd38278e4 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt @@ -16,7 +16,7 @@ import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.FlowHandle import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceType -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.success import net.corda.flows.* import net.corda.node.services.startFlowPermission diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt index eed9b556e6..c44a1b7d2e 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt @@ -27,7 +27,7 @@ import net.corda.core.identity.Party import net.corda.core.messaging.FlowHandle import net.corda.core.messaging.startFlow import net.corda.core.node.NodeInfo -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.then import net.corda.core.transactions.SignedTransaction import net.corda.explorer.formatters.PartyNameFormatter diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/CrossCashTest.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/CrossCashTest.kt index b0268fefe2..d247b614f7 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/CrossCashTest.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/CrossCashTest.kt @@ -9,7 +9,7 @@ import net.corda.core.contracts.PartyAndReference import net.corda.core.contracts.USD import net.corda.core.failure import net.corda.core.identity.AbstractParty -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.success import net.corda.flows.CashFlowCommand import net.corda.loadtest.LoadTest diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/GenerateHelpers.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/GenerateHelpers.kt index 32442df9c1..e3ede62d99 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/GenerateHelpers.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/GenerateHelpers.kt @@ -7,7 +7,7 @@ import net.corda.core.contracts.Issued import net.corda.core.contracts.PartyAndReference import net.corda.core.contracts.withoutIssuer import net.corda.core.identity.Party -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.flows.CashFlowCommand import java.util.* diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/StabilityTest.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/StabilityTest.kt index f15e073733..e9508cf324 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/StabilityTest.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/StabilityTest.kt @@ -6,7 +6,7 @@ import net.corda.core.contracts.USD import net.corda.core.failure import net.corda.core.flows.FlowException import net.corda.core.getOrThrow -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.success import net.corda.core.utilities.loggerFor import net.corda.flows.CashFlowCommand diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt index bdbf03aeb9..da594e5422 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt @@ -6,7 +6,7 @@ import net.corda.core.contracts.DOLLARS import net.corda.core.map import net.corda.core.messaging.startFlow import net.corda.core.node.services.ServiceInfo -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.WireTransaction import net.corda.testing.ALICE From 984fbd89957d41c47d4b09bd392b2592df2adaf1 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Thu, 6 Jul 2017 17:44:47 +0100 Subject: [PATCH 70/97] Moved loggerFor and other useful Kotlin extensions into KotilnUtils.kt and moved LogHelper into test-utils --- .../net/corda/core/utilities/KotlinUtils.kt | 21 +++++++++++++++++++ .../net/corda/contracts/asset/CashTests.kt | 1 - .../node/messaging/TwoPartyTradeFlowTests.kt | 2 +- .../messaging/ArtemisMessagingTests.kt | 2 +- .../persistence/DBCheckpointStorageTests.kt | 2 +- .../persistence/DBTransactionStorageTests.kt | 2 +- .../persistence/NodeAttachmentStorageTest.kt | 2 +- .../services/schema/HibernateObserverTests.kt | 4 +--- .../statemachine/FlowFrameworkTests.kt | 2 +- .../DistributedImmutableMapTests.kt | 3 +-- .../PersistentUniquenessProviderTests.kt | 3 +-- .../services/vault/NodeVaultServiceTest.kt | 2 +- .../node/services/vault/VaultWithCashTest.kt | 2 +- .../corda/irs/api/NodeInterestRatesTest.kt | 2 +- .../netmap/simulation/IRSSimulationTest.kt | 2 +- .../kotlin/net/corda/testing/LogHelper.kt | 16 +------------- 16 files changed, 35 insertions(+), 33 deletions(-) create mode 100644 core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt rename core/src/main/kotlin/net/corda/core/utilities/Logging.kt => test-utils/src/main/kotlin/net/corda/testing/LogHelper.kt (80%) diff --git a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt new file mode 100644 index 0000000000..e6e656a199 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt @@ -0,0 +1,21 @@ +package net.corda.core.utilities + +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +/** + * Get the [Logger] for a class using the syntax + * + * `val logger = loggerFor()` + */ +inline fun loggerFor(): Logger = LoggerFactory.getLogger(T::class.java) + +/** Log a TRACE level message produced by evaluating the given lamdba, but only if TRACE logging is enabled. */ +inline fun Logger.trace(msg: () -> String) { + if (isTraceEnabled) trace(msg()) +} + +/** Log a DEBUG level message produced by evaluating the given lamdba, but only if DEBUG logging is enabled. */ +inline fun Logger.debug(msg: () -> String) { + if (isDebugEnabled) debug(msg()) +} \ No newline at end of file diff --git a/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt b/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt index c27a2b4856..d08d9e36c4 100644 --- a/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt @@ -13,7 +13,6 @@ import net.corda.core.node.services.unconsumedStates import net.corda.core.utilities.OpaqueBytes import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.* import net.corda.node.services.vault.NodeVaultService import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index 8e4e9611ba..d50a09fcd3 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -26,7 +26,7 @@ import net.corda.core.serialization.serialize import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.LogHelper +import net.corda.testing.LogHelper import net.corda.core.utilities.unwrap import net.corda.flows.TwoPartyTradeFlow.Buyer import net.corda.flows.TwoPartyTradeFlow.Seller diff --git a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt index 4a4a0cc7e3..b93802356b 100644 --- a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt @@ -8,7 +8,7 @@ import com.google.common.util.concurrent.SettableFuture import net.corda.core.crypto.generateKeyPair import net.corda.core.messaging.RPCOps import net.corda.core.node.services.DEFAULT_SESSION_ID -import net.corda.core.utilities.LogHelper +import net.corda.testing.LogHelper import net.corda.node.services.RPCUserService import net.corda.node.services.RPCUserServiceImpl import net.corda.node.services.api.MonitoringService diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBCheckpointStorageTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBCheckpointStorageTests.kt index 095f20f9cb..92180fde67 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBCheckpointStorageTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBCheckpointStorageTests.kt @@ -2,7 +2,7 @@ package net.corda.node.services.persistence import com.google.common.primitives.Ints import net.corda.core.serialization.SerializedBytes -import net.corda.core.utilities.LogHelper +import net.corda.testing.LogHelper import net.corda.node.services.api.Checkpoint import net.corda.node.services.api.CheckpointStorage import net.corda.node.services.transactions.PersistentUniquenessProvider diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt index fe4acd24b6..c98f62dfa3 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt @@ -9,7 +9,7 @@ import net.corda.core.toFuture import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction import net.corda.testing.DUMMY_NOTARY -import net.corda.core.utilities.LogHelper +import net.corda.testing.LogHelper import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt index 0246f69f64..f50738f6c3 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt @@ -7,7 +7,7 @@ import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 import net.corda.core.read import net.corda.core.readAll -import net.corda.core.utilities.LogHelper +import net.corda.testing.LogHelper import net.corda.core.write import net.corda.core.writeLines import net.corda.node.services.database.RequeryConfiguration diff --git a/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt b/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt index 8c087f2b1c..6cb9b6c47c 100644 --- a/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt @@ -1,17 +1,15 @@ package net.corda.node.services.schema import net.corda.core.contracts.* -import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.SecureHash import net.corda.core.identity.AbstractParty import net.corda.core.node.services.Vault import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState import net.corda.core.schemas.QueryableState -import net.corda.core.utilities.LogHelper +import net.corda.testing.LogHelper import net.corda.node.services.api.SchemaService import net.corda.node.services.database.HibernateConfiguration -import net.corda.node.services.schema.HibernateObserver import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction import net.corda.testing.MEGA_CORP diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt index 9773a7f733..e59e703dec 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt @@ -25,7 +25,7 @@ import net.corda.core.utilities.OpaqueBytes import net.corda.core.serialization.deserialize import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder -import net.corda.core.utilities.LogHelper +import net.corda.testing.LogHelper import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker.Change import net.corda.core.utilities.unwrap diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/DistributedImmutableMapTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/DistributedImmutableMapTests.kt index 3500211147..fe57ded32c 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/DistributedImmutableMapTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/DistributedImmutableMapTests.kt @@ -8,9 +8,8 @@ import io.atomix.copycat.server.CopycatServer import io.atomix.copycat.server.storage.Storage import io.atomix.copycat.server.storage.StorageLevel import net.corda.core.getOrThrow -import net.corda.core.utilities.LogHelper +import net.corda.testing.LogHelper import net.corda.node.services.network.NetworkMapService -import net.corda.node.services.transactions.DistributedImmutableMap import net.corda.node.utilities.configureDatabase import net.corda.testing.freeLocalHostAndPort import net.corda.testing.node.makeTestDataSourceProperties diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt index 4f14caa074..1ff8c103f3 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt @@ -2,8 +2,7 @@ package net.corda.node.services.transactions import net.corda.core.crypto.SecureHash import net.corda.core.node.services.UniquenessException -import net.corda.core.utilities.LogHelper -import net.corda.node.services.transactions.PersistentUniquenessProvider +import net.corda.testing.LogHelper import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction import net.corda.testing.MEGA_CORP diff --git a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt index 8ce32149e0..1f52670c79 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt @@ -11,7 +11,7 @@ import net.corda.core.node.services.unconsumedStates import net.corda.core.utilities.OpaqueBytes import net.corda.core.transactions.SignedTransaction import net.corda.testing.DUMMY_NOTARY -import net.corda.core.utilities.LogHelper +import net.corda.testing.LogHelper import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction import net.corda.testing.BOC diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt index 1c3de0094f..50314208d4 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt @@ -16,7 +16,7 @@ import net.corda.core.transactions.SignedTransaction import net.corda.testing.BOB import net.corda.testing.DUMMY_NOTARY import net.corda.testing.DUMMY_NOTARY_KEY -import net.corda.core.utilities.LogHelper +import net.corda.testing.LogHelper import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction import net.corda.testing.MEGA_CORP diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt index d9113124ae..9d3035cef3 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt @@ -14,7 +14,7 @@ import net.corda.core.getOrThrow import net.corda.core.identity.Party import net.corda.core.node.services.ServiceInfo import net.corda.core.transactions.TransactionBuilder -import net.corda.core.utilities.LogHelper +import net.corda.testing.LogHelper import net.corda.core.utilities.ProgressTracker import net.corda.irs.flows.RatesFixFlow import net.corda.node.utilities.configureDatabase diff --git a/samples/network-visualiser/src/test/kotlin/net/corda/netmap/simulation/IRSSimulationTest.kt b/samples/network-visualiser/src/test/kotlin/net/corda/netmap/simulation/IRSSimulationTest.kt index 97777c9dbe..6fc24d24e3 100644 --- a/samples/network-visualiser/src/test/kotlin/net/corda/netmap/simulation/IRSSimulationTest.kt +++ b/samples/network-visualiser/src/test/kotlin/net/corda/netmap/simulation/IRSSimulationTest.kt @@ -1,7 +1,7 @@ package net.corda.netmap.simulation import net.corda.core.getOrThrow -import net.corda.core.utilities.LogHelper +import net.corda.testing.LogHelper import org.junit.Test class IRSSimulationTest { diff --git a/core/src/main/kotlin/net/corda/core/utilities/Logging.kt b/test-utils/src/main/kotlin/net/corda/testing/LogHelper.kt similarity index 80% rename from core/src/main/kotlin/net/corda/core/utilities/Logging.kt rename to test-utils/src/main/kotlin/net/corda/testing/LogHelper.kt index f17b1cc5e7..ad2e488e91 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/Logging.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/LogHelper.kt @@ -1,26 +1,12 @@ -package net.corda.core.utilities +package net.corda.testing import org.apache.logging.log4j.Level import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.core.LoggerContext import org.apache.logging.log4j.core.config.Configurator import org.apache.logging.log4j.core.config.LoggerConfig -import org.slf4j.LoggerFactory import kotlin.reflect.KClass -// A couple of inlined utility functions: the first is just a syntax convenience, the second lets us use -// Kotlin's string interpolation efficiently: the message is never calculated/concatenated together unless -// logging at that level is enabled. -inline fun loggerFor(): org.slf4j.Logger = LoggerFactory.getLogger(T::class.java) - -inline fun org.slf4j.Logger.trace(msg: () -> String) { - if (isTraceEnabled) trace(msg()) -} - -inline fun org.slf4j.Logger.debug(msg: () -> String) { - if (isDebugEnabled) debug(msg()) -} - /** A configuration helper that allows modifying the log level for specific loggers */ object LogHelper { /** From 67ccf69dbbb19a6d8a5be6f7f715d12e52521436 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Fri, 7 Jul 2017 12:06:10 +0100 Subject: [PATCH 71/97] Joel integrate cookbook into TX API --- docs/source/api-transactions.rst | 574 +++++++++++++----- .../java/net/corda/docs/FlowCookbookJava.java | 133 +++- .../kotlin/net/corda/docs/FlowCookbook.kt | 121 +++- 3 files changed, 663 insertions(+), 165 deletions(-) diff --git a/docs/source/api-transactions.rst b/docs/source/api-transactions.rst index 9b736b3ce8..a9a62e89e8 100644 --- a/docs/source/api-transactions.rst +++ b/docs/source/api-transactions.rst @@ -43,10 +43,10 @@ Transaction workflow -------------------- There are four states the transaction can occupy: -* ``TransactionBuilder``, a mutable transaction-in-construction +* ``TransactionBuilder``, a builder for a transaction in construction * ``WireTransaction``, an immutable transaction -* ``SignedTransaction``, a ``WireTransaction`` with 1+ associated signatures -* ``LedgerTransaction``, a resolved ``WireTransaction`` that can be checked for contract validity +* ``SignedTransaction``, an immutable transaction with 1+ associated signatures +* ``LedgerTransaction``, a transaction that can be checked for validity Here are the possible transitions between transaction states: @@ -56,25 +56,198 @@ TransactionBuilder ------------------ Creating a builder ^^^^^^^^^^^^^^^^^^ -The first step when building a transaction is to create a ``TransactionBuilder``: +The first step when creating a transaction is to instantiate a ``TransactionBuilder``. We can create a builder for each +transaction type as follows: .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 19 + :end-before: DOCEND 19 + :dedent: 12 - // A general transaction builder. - val generalTxBuilder = TransactionType.General.Builder() + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 19 + :end-before: DOCEND 19 + :dedent: 12 - // A notary-change transaction builder. - val notaryChangeTxBuilder = TransactionType.NotaryChange.Builder() +Transaction components +^^^^^^^^^^^^^^^^^^^^^^ +Once we have a ``TransactionBuilder``, we need to gather together the various transaction components the transaction +will include. - .. sourcecode:: java +Input states +~~~~~~~~~~~~ +Input states are added to a transaction as ``StateAndRef`` instances. A ``StateAndRef`` combines: - // A general transaction builder. - final TransactionBuilder generalTxBuilder = new TransactionType.General.Builder(); +* A ``ContractState`` representing the input state itself +* A ``StateRef`` pointing to the input among the outputs of the transaction that created it - // A notary-change transaction builder. - final TransactionBuilder notaryChangeTxBuilder = new TransactionType.NotaryChange.Builder(); +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 21 + :end-before: DOCEND 21 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 21 + :end-before: DOCEND 21 + :dedent: 12 + +A ``StateRef`` uniquely identifies an input state, allowing the notary to mark it as historic. It is made up of: + +* The hash of the transaction that generated the state +* The state's index in the outputs of that transaction + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 20 + :end-before: DOCEND 20 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 20 + :end-before: DOCEND 20 + :dedent: 12 + +The ``StateRef`` create a chain of pointers from the input states back to the transactions that created them. This +allows a node to work backwards and verify the entirety of the transaction chain. + +Output states +~~~~~~~~~~~~~ +Since a transaction's output states do not exist until the transaction is committed, they cannot be referenced as the +outputs of previous transactions. Instead, we create the desired output states as ``ContractState`` instances, and +add them to the transaction: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 22 + :end-before: DOCEND 22 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 22 + :end-before: DOCEND 22 + :dedent: 12 + +In many cases (e.g. when we have a transaction that updates an existing state), we may want to create an output by +copying from the input state: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 23 + :end-before: DOCEND 23 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 23 + :end-before: DOCEND 23 + :dedent: 12 + +Commands +~~~~~~~~ +Commands are added to the transaction as ``Command`` instances. ``Command`` combines: + +* A ``CommandData`` instance representing the type of the command +* A list of the command's required signers + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 24 + :end-before: DOCEND 24 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 24 + :end-before: DOCEND 24 + :dedent: 12 + +Attachments +~~~~~~~~~~~ +Attachments are identified by their hash. The attachment with the corresponding hash must have been uploaded ahead of +time via the node's RPC interface: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 25 + :end-before: DOCEND 25 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 25 + :end-before: DOCEND 25 + :dedent: 12 + +Time-windows +~~~~~~~~~~~~ +Time windows represent the period of time during which the transaction must be notarised. They can have a start and an +end time, or be open at either end: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 26 + :end-before: DOCEND 26 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 26 + :end-before: DOCEND 26 + :dedent: 12 + +We can also define a time window as an ``Instant`` +/- a time tolerance (e.g. 30 seconds): + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 42 + :end-before: DOCEND 42 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 42 + :end-before: DOCEND 42 + :dedent: 12 + +Or as a start-time plus a duration: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 43 + :end-before: DOCEND 43 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 43 + :end-before: DOCEND 43 + :dedent: 12 Adding items ^^^^^^^^^^^^ @@ -95,56 +268,69 @@ The transaction builder is mutable. We add items to it using the ``TransactionBu Passing in objects of any other type will cause an ``IllegalArgumentException`` to be thrown. -You can also add the following items to the transaction: - -* ``TimeWindow`` objects, using ``TransactionBuilder.setTime`` -* ``SecureHash`` objects referencing the hash of an attachment stored on the node, using - ``TransactionBuilder.addAttachment`` - -Input states -~~~~~~~~~~~~ -Input states are added to a transaction as ``StateAndRef`` instances, rather than as ``ContractState`` instances. - -A ``StateAndRef`` combines a ``ContractState`` with a pointer to the transaction that created it. This series of -pointers from the input states back to the transactions that created them is what allows a node to work backwards and -verify the entirety of the transaction chain. It is defined as: +Here's an example usage of ``TransactionBuilder.withItems``: .. container:: codeset - .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/contracts/Structures.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt :language: kotlin - :start-after: DOCSTART 7 - :end-before: DOCEND 7 + :start-after: DOCSTART 27 + :end-before: DOCEND 27 + :dedent: 12 -Where ``StateRef`` is defined as: + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 27 + :end-before: DOCEND 27 + :dedent: 12 + +You can also pass in objects one-by-one. This is the only way to add attachments: .. container:: codeset - .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/contracts/Structures.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt :language: kotlin - :start-after: DOCSTART 8 - :end-before: DOCEND 8 + :start-after: DOCSTART 28 + :end-before: DOCEND 28 + :dedent: 12 -``StateRef.index`` is the state's position in the outputs of the transaction that created it. In this way, a -``StateRef`` allows a notary service to uniquely identify the existing states that a transaction is marking as historic. + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 28 + :end-before: DOCEND 28 + :dedent: 12 -Output states -~~~~~~~~~~~~~ -Since a transaction's output states do not exist until the transaction is committed, they cannot be referenced as the -outputs of previous transactions. Instead, we create the desired output states as ``ContractState`` instances, and -add them to the transaction. - -Commands -~~~~~~~~ -Commands are added to the transaction as ``Command`` instances. ``Command`` combines a ``CommandData`` -instance representing the type of the command with a list of the command's required signers. It is defined as: +To set the transaction builder's time-window, we can either set a time-window directly: .. container:: codeset - .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/contracts/Structures.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt :language: kotlin - :start-after: DOCSTART 9 - :end-before: DOCEND 9 + :start-after: DOCSTART 44 + :end-before: DOCEND 44 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 44 + :end-before: DOCEND 44 + :dedent: 12 + +Or define the time-window as a time plus a duration (e.g. 45 seconds): + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 45 + :end-before: DOCEND 45 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 45 + :end-before: DOCEND 45 + :dedent: 12 Signing the builder ^^^^^^^^^^^^^^^^^^^ @@ -152,32 +338,42 @@ Once the builder is ready, we finalize it by signing it and converting it into a .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 29 + :end-before: DOCEND 29 + :dedent: 12 - // Finalizes the builder by signing it with our primary signing key. - val signedTx1 = serviceHub.signInitialTransaction(unsignedTx) + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 29 + :end-before: DOCEND 29 + :dedent: 12 - // Finalizes the builder by signing it with a different key. - val signedTx2 = serviceHub.signInitialTransaction(unsignedTx, otherKey) +This will sign the transaction with your legal identity key. You can also choose to use another one of your public keys: - // Finalizes the builder by signing it with a set of keys. - val signedTx3 = serviceHub.signInitialTransaction(unsignedTx, otherKeys) +.. container:: codeset - .. sourcecode:: java + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 30 + :end-before: DOCEND 30 + :dedent: 12 - // Finalizes the builder by signing it with our primary signing key. - final SignedTransaction signedTx1 = getServiceHub().signInitialTransaction(unsignedTx); + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 30 + :end-before: DOCEND 30 + :dedent: 12 - // Finalizes the builder by signing it with a different key. - final SignedTransaction signedTx2 = getServiceHub().signInitialTransaction(unsignedTx, otherKey); - - // Finalizes the builder by signing it with a set of keys. - final SignedTransaction signedTx3 = getServiceHub().signInitialTransaction(unsignedTx, otherKeys); +Either way, the outcome of this process is to create a ``SignedTransaction``, which can no longer be modified. SignedTransaction ----------------- -A ``SignedTransaction`` is a combination of an immutable ``WireTransaction`` and a list of signatures over that -transaction: +A ``SignedTransaction`` is a combination of: + +* An immutable ``WireTransaction`` +* A list of signatures over that transaction .. container:: codeset @@ -186,114 +382,210 @@ transaction: :start-after: DOCSTART 1 :end-before: DOCEND 1 +Before adding our signature to the transaction, we'll want to verify both the transaction itself and its signatures. + +Verifying the transaction +^^^^^^^^^^^^^^^^^^^^^^^^^ +To verify a transaction, we need to retrieve any states in the transaction chain that our node doesn't +currently have in its local storage from the proposer(s) of the transaction. This process is handled by a built-in flow +called ``ResolveTransactionsFlow``. See :doc:`api-flows` for more details. + +When verifying a ``SignedTransaction``, we don't verify the ``SignedTransaction`` *per se*, but rather the +``WireTransaction`` it contains. We extract this ``WireTransaction`` as follows: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 31 + :end-before: DOCEND 31 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 31 + :end-before: DOCEND 31 + :dedent: 12 + +However, this still isn't enough. The ``WireTransaction`` holds its inputs as ``StateRef`` instances, and its +attachments as hashes. These do not provide enough information to properly validate the transaction's contents. To +resolve these into actual ``ContractState`` and ``Attachment`` instances, we need to use the ``ServiceHub`` to convert +the ``WireTransaction`` into a ``LedgerTransaction``: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 32 + :end-before: DOCEND 32 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 32 + :end-before: DOCEND 32 + :dedent: 12 + +We can now *verify* the transaction to ensure that it satisfies the contracts of all the transaction's input and output +states: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 33 + :end-before: DOCEND 33 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 33 + :end-before: DOCEND 33 + :dedent: 12 + +We will generally also want to conduct some additional validation of the transaction, beyond what is provided for in +the contract. Here's an example of how we might do this: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 34 + :end-before: DOCEND 34 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 34 + :end-before: DOCEND 34 + :dedent: 12 + Verifying the signatures ^^^^^^^^^^^^^^^^^^^^^^^^ -The signatures on a ``SignedTransaction`` have not necessarily been checked for validity. We check them using +We also need to verify the signatures over the transaction to prevent tampering. We do this using ``SignedTransaction.verifySignatures``: .. container:: codeset - .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt :language: kotlin - :start-after: DOCSTART 2 - :end-before: DOCEND 2 + :start-after: DOCSTART 35 + :end-before: DOCEND 35 + :dedent: 12 -``verifySignatures`` takes a ``vararg`` of the public keys for which the signatures are allowed to be missing. If the -transaction is missing any signatures without the corresponding public keys being passed in, a + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 35 + :end-before: DOCEND 35 + :dedent: 12 + +Optionally, we can pass ``verifySignatures`` a ``vararg`` of the public keys for which the signatures are allowed +to be missing: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 36 + :end-before: DOCEND 36 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 36 + :end-before: DOCEND 36 + :dedent: 12 + +If the transaction is missing any signatures without the corresponding public keys being passed in, a ``SignaturesMissingException`` is thrown. -Verifying the transaction -^^^^^^^^^^^^^^^^^^^^^^^^^ -Verifying a transaction is a multi-step process: - -* We check the transaction's signatures: +We can also choose to simply verify the signatures that are present: .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 37 + :end-before: DOCEND 37 + :dedent: 12 - subFlow(ResolveTransactionsFlow(transactionToVerify, partyWithTheFullChain)) + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 37 + :end-before: DOCEND 37 + :dedent: 12 - .. sourcecode:: java - - subFlow(new ResolveTransactionsFlow(transactionToVerify, partyWithTheFullChain)); - -* Before verifying the transaction, we need to retrieve from the proposer(s) of the transaction any parts of the - transaction chain that our node doesn't currently have in its local storage: - -.. container:: codeset - - .. sourcecode:: kotlin - - subFlow(ResolveTransactionsFlow(transactionToVerify, partyWithTheFullChain)) - - .. sourcecode:: java - - subFlow(new ResolveTransactionsFlow(transactionToVerify, partyWithTheFullChain)); - -* To verify the transaction, we first need to resolve any state references and attachment hashes by converting the - ``SignedTransaction`` into a ``LedgerTransaction``. We can then verify the fully-resolved transaction: - -.. container:: codeset - - .. sourcecode:: kotlin - - partSignedTx.tx.toLedgerTransaction(serviceHub).verify() - - .. sourcecode:: java - - partSignedTx.getTx().toLedgerTransaction(getServiceHub()).verify(); - -* We will generally also want to conduct some custom validation of the transaction, beyond what is provided for in the - contract: - -.. container:: codeset - - .. sourcecode:: kotlin - - val ledgerTransaction = partSignedTx.tx.toLedgerTransaction(serviceHub) - val inputStateAndRef = ledgerTransaction.inputs.single() - val input = inputStateAndRef.state.data as MyState - if (input.value > 1000000) { - throw FlowException("Proposed input value too high!") - } - - .. sourcecode:: java - - final LedgerTransaction ledgerTransaction = partSignedTx.getTx().toLedgerTransaction(getServiceHub()); - final StateAndRef inputStateAndRef = ledgerTransaction.getInputs().get(0); - final MyState input = (MyState) inputStateAndRef.getState().getData(); - if (input.getValue() > 1000000) { - throw new FlowException("Proposed input value too high!"); - } +However, BE VERY CAREFUL - this function provides no guarantees that the signatures are correct, or that none are +missing. Signing the transaction ^^^^^^^^^^^^^^^^^^^^^^^ -We add an additional signature to an existing ``SignedTransaction`` using: +Once we are satisfied with the contents and existing signatures over the transaction, we can add our signature to the +``SignedTransaction`` using: .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 38 + :end-before: DOCEND 38 + :dedent: 12 - val fullySignedTx = serviceHub.addSignature(partSignedTx) + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 38 + :end-before: DOCEND 38 + :dedent: 12 - .. sourcecode:: java +As with the ``TransactionBuilder``, we can also choose to sign using another one of our public keys: - SignedTransaction fullySignedTx = getServiceHub().addSignature(partSignedTx); +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 39 + :end-before: DOCEND 39 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 39 + :end-before: DOCEND 39 + :dedent: 12 We can also generate a signature over the transaction without adding it to the transaction directly by using: .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 40 + :end-before: DOCEND 40 + :dedent: 12 - val signature = serviceHub.createSignature(partSignedTx) + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 40 + :end-before: DOCEND 40 + :dedent: 12 - .. sourcecode:: java +Or using another one of our public keys, as follows: - DigitalSignature.WithKey signature = getServiceHub().createSignature(partSignedTx); +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 41 + :end-before: DOCEND 41 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 41 + :end-before: DOCEND 41 + :dedent: 12 Notarising and recording ^^^^^^^^^^^^^^^^^^^^^^^^ Notarising and recording a transaction is handled by a built-in flow called ``FinalityFlow``. See -:doc:`api-flows` for more details. \ No newline at end of file +:doc:`api-flows` for more details. diff --git a/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java b/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java index be0cd0e03e..8d5d16364d 100644 --- a/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java +++ b/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java @@ -9,6 +9,7 @@ import net.corda.core.contracts.TransactionType.General; import net.corda.core.contracts.TransactionType.NotaryChange; import net.corda.core.contracts.testing.DummyContract; import net.corda.core.contracts.testing.DummyState; +import net.corda.core.crypto.DigitalSignature; import net.corda.core.crypto.SecureHash; import net.corda.core.flows.*; import net.corda.core.identity.Party; @@ -30,6 +31,7 @@ import net.corda.flows.SignTransactionFlow; import org.bouncycastle.asn1.x500.X500Name; import java.security.PublicKey; +import java.security.SignatureException; import java.time.Duration; import java.time.Instant; import java.util.List; @@ -83,6 +85,7 @@ public class FlowCookbookJava { return CollectSignaturesFlow.Companion.tracker(); } }; + private static final Step VERIFYING_SIGS = new Step("Verifying a transaction's signatures."); private static final Step FINALISATION = new Step("Finalising a transaction.") { @Override public ProgressTracker childProgressTracker() { return FinalityFlow.Companion.tracker(); @@ -236,10 +239,16 @@ public class FlowCookbookJava { // When building a transaction, input states are passed in as // ``StateRef`` instances, which pair the hash of the transaction // that generated the state with the state's index in the outputs - // of that transaction. + // of that transaction. In practice, we'd pass the transaction hash + // or the ``StateRef`` as a parameter to the flow, or extract the + // ``StateRef`` from our vault. + // DOCSTART 20 StateRef ourStateRef = new StateRef(SecureHash.sha256("DummyTransactionHash"), 0); + // DOCEND 20 // A ``StateAndRef`` pairs a ``StateRef`` with the state it points to. + // DOCSTART 21 StateAndRef ourStateAndRef = getServiceHub().toStateAndRef(ourStateRef); + // DOCEND 21 /*------------------------------------------ * GATHERING OTHER TRANSACTION COMPONENTS * @@ -247,18 +256,24 @@ public class FlowCookbookJava { progressTracker.setCurrentStep(OTHER_TX_COMPONENTS); // Output states are constructed from scratch. + // DOCSTART 22 DummyState ourOutput = new DummyState(); + // DOCEND 22 // Or as copies of other states with some properties changed. + // DOCSTART 23 DummyState ourOtherOutput = ourOutput.copy(77); + // DOCEND 23 // Commands pair a ``CommandData`` instance with a list of // public keys. To be valid, the transaction requires a signature // matching every public key in all of the transaction's commands. + // DOCSTART 24 CommandData commandData = new DummyContract.Commands.Create(); PublicKey ourPubKey = getServiceHub().getLegalIdentityKey(); PublicKey counterpartyPubKey = counterparty.getOwningKey(); List requiredSigners = ImmutableList.of(ourPubKey, counterpartyPubKey); Command ourCommand = new Command(commandData, requiredSigners); + // DOCEND 24 // ``CommandData`` can either be: // 1. Of type ``TypeOnlyCommandData``, in which case it only @@ -272,12 +287,28 @@ public class FlowCookbookJava { // Attachments are identified by their hash. // The attachment with the corresponding hash must have been // uploaded ahead of time via the node's RPC interface. + // DOCSTART 25 SecureHash ourAttachment = SecureHash.sha256("DummyAttachment"); + // DOCEND 25 - // Time windows can have a start and end time, or be open at either end. + // Time windows represent the period of time during which a + // transaction must be notarised. They can have a start and an end + // time, or be open at either end. + // DOCSTART 26 TimeWindow ourTimeWindow = TimeWindow.between(Instant.MIN, Instant.MAX); TimeWindow ourAfter = TimeWindow.fromOnly(Instant.MIN); TimeWindow ourBefore = TimeWindow.untilOnly(Instant.MAX); + // DOCEND 26 + + // We can also define a time window as an ``Instant`` +/- a time + // tolerance (e.g. 30 seconds): + // DOCSTART 42 + TimeWindow ourTimeWindow2 = TimeWindow.withTolerance(Instant.now(), Duration.ofSeconds(30)); + // DOCEND 42 + // Or as a start-time plus a duration: + // DOCSTART 43 + TimeWindow ourTimeWindow3 = TimeWindow.fromStartAndDuration(Instant.now(), Duration.ofSeconds(30)); + // DOCEND 43 /*------------------------ * TRANSACTION BUILDING * @@ -286,28 +317,40 @@ public class FlowCookbookJava { // There are two types of transaction (notary-change and general), // and therefore two types of transaction builder: + // DOCSTART 19 TransactionBuilder notaryChangeTxBuilder = new TransactionBuilder(NotaryChange.INSTANCE, specificNotary); TransactionBuilder regTxBuilder = new TransactionBuilder(General.INSTANCE, specificNotary); + // DOCEND 19 // We add items to the transaction builder using ``TransactionBuilder.withItems``: + // DOCSTART 27 regTxBuilder.withItems( // Inputs, as ``StateRef``s that reference to the outputs of previous transactions - ourStateRef, + ourStateAndRef, // Outputs, as ``ContractState``s ourOutput, // Commands, as ``Command``s ourCommand ); + // DOCEND 27 // We can also add items using methods for the individual components: + // DOCSTART 28 regTxBuilder.addInputState(ourStateAndRef); regTxBuilder.addOutputState(ourOutput); regTxBuilder.addCommand(ourCommand); regTxBuilder.addAttachment(ourAttachment); + // DOCEND 28 - // We set the time-window within which the transaction must be notarised using either of: + // There are several ways of setting the transaction's time-window. + // We can set a time-window directly: + // DOCSTART 44 regTxBuilder.setTimeWindow(ourTimeWindow); - regTxBuilder.setTimeWindow(getServiceHub().getClock().instant(), Duration.ofSeconds(30)); + // DOCEND 44 + // Or as a start time plus a duration (e.g. 45 seconds): + // DOCSTART 45 + regTxBuilder.setTimeWindow(Instant.now(), Duration.ofSeconds(45)); + // DOCEND 45 /*----------------------- * TRANSACTION SIGNING * @@ -316,12 +359,40 @@ public class FlowCookbookJava { // We finalise the transaction by signing it, // converting it into a ``SignedTransaction``. + // DOCSTART 29 SignedTransaction onceSignedTx = getServiceHub().signInitialTransaction(regTxBuilder); + // DOCEND 29 + // We can also sign the transaction using a different public key: + // DOCSTART 30 + PublicKey otherKey = getServiceHub().getKeyManagementService().freshKey(); + SignedTransaction onceSignedTx2 = getServiceHub().signInitialTransaction(regTxBuilder, otherKey); + // DOCEND 30 // If instead this was a ``SignedTransaction`` that we'd received // from a counterparty and we needed to sign it, we would add our // signature using: - SignedTransaction twiceSignedTx = getServiceHub().addSignature(onceSignedTx, dummyPubKey); + // DOCSTART 38 + SignedTransaction twiceSignedTx = getServiceHub().addSignature(onceSignedTx); + // DOCEND 38 + // Or, if we wanted to use a different public key: + PublicKey otherKey2 = getServiceHub().getKeyManagementService().freshKey(); + // DOCSTART 39 + SignedTransaction twiceSignedTx2 = getServiceHub().addSignature(onceSignedTx, otherKey2); + // DOCEND 39 + + // We can also generate a signature over the transaction without + // adding it to the transaction itself. We may do this when + // sending just the signature in a flow instead of returning the + // entire transaction with our signature. This way, the receiving + // node does not need to check we haven't changed anything in the + // transaction. + // DOCSTART 40 + DigitalSignature.WithKey sig = getServiceHub().createSignature(onceSignedTx); + // DOCEND 40 + // And again, if we wanted to use a different public key: + // DOCSTART 41 + DigitalSignature.WithKey sig2 = getServiceHub().createSignature(onceSignedTx, otherKey2); + // DOCEND 41 /*---------------------------- * TRANSACTION VERIFICATION * @@ -342,34 +413,41 @@ public class FlowCookbookJava { subFlow(new ResolveTransactionsFlow(ImmutableSet.of(ourStateRef.getTxhash()), counterparty)); // DOCEND 14 - // We verify a transaction using the following one-liner: - twiceSignedTx.getTx().toLedgerTransaction(getServiceHub()).verify(); - - // Let's break that down... - // A ``SignedTransaction`` is a pairing of a ``WireTransaction`` // with signatures over this ``WireTransaction``. We don't verify // a signed transaction per se, but rather the ``WireTransaction`` // it contains. + // DOCSTART 31 WireTransaction wireTx = twiceSignedTx.getTx(); + // DOCEND 31 // Before we can verify the transaction, we need the // ``ServiceHub`` to use our node's local storage to resolve the // transaction's inputs and attachments into actual objects, // rather than just references. We do this by converting the // ``WireTransaction`` into a ``LedgerTransaction``. + // DOCSTART 32 LedgerTransaction ledgerTx = wireTx.toLedgerTransaction(getServiceHub()); + // DOCEND 32 // We can now verify the transaction. + // DOCSTART 33 ledgerTx.verify(); + // DOCEND 33 // We'll often want to perform our own additional verification // too. Just because a transaction is valid based on the contract // rules and requires our signature doesn't mean we have to // sign it! We need to make sure the transaction represents an // agreement we actually want to enter into. + // DOCSTART 34 DummyState outputState = (DummyState) wireTx.getOutputs().get(0).getData(); if (outputState.getMagicNumber() != 777) { + // ``FlowException`` is a special exception type. It will be + // propagated back to any counterparty flows waiting for a + // message from this flow, notifying them that the flow has + // failed. throw new FlowException("We expected a magic number of 777."); } + // DOCEND 34 // Of course, if you are not a required signer on the transaction, // you have no power to decide whether it is valid or not. If it @@ -390,6 +468,37 @@ public class FlowCookbookJava { SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(twiceSignedTx, SIGS_GATHERING.childProgressTracker())); // DOCEND 15 + /*------------------------ + * VERIFYING SIGNATURES * + ------------------------*/ + progressTracker.setCurrentStep(VERIFYING_SIGS); + + try { + + // We can verify that a transaction has all the required + // signatures, and that they're all valid, by running: + // DOCSTART 35 + fullySignedTx.verifySignatures(); + // DOCEND 35 + + // If the transaction is only partially signed, we have to pass in + // a list of the public keys corresponding to the missing + // signatures, explicitly telling the system not to check them. + // DOCSTART 36 + onceSignedTx.verifySignatures(counterpartyPubKey); + // DOCEND 36 + + // We can also choose to only check the signatures that are + // present. BE VERY CAREFUL - this function provides no guarantees + // that the signatures are correct, or that none are missing. + // DOCSTART 37 + twiceSignedTx.checkSignaturesAreValid(); + // DOCEND 37 + + } catch (SignatureException e) { + // Handle this as required. + } + /*------------------------------ * FINALISING THE TRANSACTION * ------------------------------*/ @@ -501,4 +610,4 @@ public class FlowCookbookJava { return null; } } -} \ No newline at end of file +} diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt index 677b9b12b9..14053980cc 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt @@ -7,6 +7,7 @@ import net.corda.core.contracts.TransactionType.General import net.corda.core.contracts.TransactionType.NotaryChange import net.corda.core.contracts.testing.DummyContract import net.corda.core.contracts.testing.DummyState +import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.SecureHash import net.corda.core.flows.* import net.corda.core.identity.Party @@ -66,6 +67,7 @@ object FlowCookbook { // subflow's progress steps in our flow's progress tracker. override fun childProgressTracker() = CollectSignaturesFlow.tracker() } + object VERIFYING_SIGS : Step("Verifying a transaction's signatures.") object FINALISATION : Step("Finalising a transaction.") { override fun childProgressTracker() = FinalityFlow.tracker() } @@ -79,6 +81,7 @@ object FlowCookbook { TX_SIGNING, TX_VERIFICATION, SIGS_GATHERING, + VERIFYING_SIGS, FINALISATION ) } @@ -219,10 +222,16 @@ object FlowCookbook { // When building a transaction, input states are passed in as // ``StateRef`` instances, which pair the hash of the transaction // that generated the state with the state's index in the outputs - // of that transaction. + // of that transaction. In practice, we'd pass the transaction hash + // or the ``StateRef`` as a parameter to the flow, or extract the + // ``StateRef`` from our vault. + // DOCSTART 20 val ourStateRef: StateRef = StateRef(SecureHash.sha256("DummyTransactionHash"), 0) + // DOCEND 20 // A ``StateAndRef`` pairs a ``StateRef`` with the state it points to. + // DOCSTART 21 val ourStateAndRef: StateAndRef = serviceHub.toStateAndRef(ourStateRef) + // DOCEND 21 /**----------------------------------------- * GATHERING OTHER TRANSACTION COMPONENTS * @@ -230,18 +239,24 @@ object FlowCookbook { progressTracker.currentStep = OTHER_TX_COMPONENTS // Output states are constructed from scratch. + // DOCSTART 22 val ourOutput: DummyState = DummyState() + // DOCEND 22 // Or as copies of other states with some properties changed. + // DOCSTART 23 val ourOtherOutput: DummyState = ourOutput.copy(magicNumber = 77) + // DOCEND 23 // Commands pair a ``CommandData`` instance with a list of // public keys. To be valid, the transaction requires a signature // matching every public key in all of the transaction's commands. + // DOCSTART 24 val commandData: CommandData = DummyContract.Commands.Create() val ourPubKey: PublicKey = serviceHub.legalIdentityKey val counterpartyPubKey: PublicKey = counterparty.owningKey val requiredSigners: List = listOf(ourPubKey, counterpartyPubKey) val ourCommand: Command = Command(commandData, requiredSigners) + // DOCEND 24 // ``CommandData`` can either be: // 1. Of type ``TypeOnlyCommandData``, in which case it only @@ -255,12 +270,26 @@ object FlowCookbook { // Attachments are identified by their hash. // The attachment with the corresponding hash must have been // uploaded ahead of time via the node's RPC interface. + // DOCSTART 25 val ourAttachment: SecureHash = SecureHash.sha256("DummyAttachment") + // DOCEND 25 // Time windows can have a start and end time, or be open at either end. + // DOCSTART 26 val ourTimeWindow: TimeWindow = TimeWindow.between(Instant.MIN, Instant.MAX) val ourAfter: TimeWindow = TimeWindow.fromOnly(Instant.MIN) val ourBefore: TimeWindow = TimeWindow.untilOnly(Instant.MAX) + // DOCEND 26 + + // We can also define a time window as an ``Instant`` +/- a time + // tolerance (e.g. 30 seconds): + // DOCSTART 42 + val ourTimeWindow2: TimeWindow = TimeWindow.withTolerance(Instant.now(), Duration.ofSeconds(30)) + // DOCEND 42 + // Or as a start-time plus a duration: + // DOCSTART 43 + val ourTimeWindow3: TimeWindow = TimeWindow.fromStartAndDuration(Instant.now(), Duration.ofSeconds(30)) + // DOCEND 43 /**----------------------- * TRANSACTION BUILDING * @@ -269,28 +298,40 @@ object FlowCookbook { // There are two types of transaction (notary-change and general), // and therefore two types of transaction builder: + // DOCSTART 19 val notaryChangeTxBuilder: TransactionBuilder = TransactionBuilder(NotaryChange, specificNotary) val regTxBuilder: TransactionBuilder = TransactionBuilder(General, specificNotary) + // DOCEND 19 // We add items to the transaction builder using ``TransactionBuilder.withItems``: + // DOCSTART 27 regTxBuilder.withItems( - // Inputs, as ``StateRef``s that reference to the outputs of previous transactions - ourStateRef, + // Inputs, as ``StateRef``s that reference the outputs of previous transactions + ourStateAndRef, // Outputs, as ``ContractState``s ourOutput, // Commands, as ``Command``s ourCommand ) + // DOCEND 27 // We can also add items using methods for the individual components: + // DOCSTART 28 regTxBuilder.addInputState(ourStateAndRef) regTxBuilder.addOutputState(ourOutput) regTxBuilder.addCommand(ourCommand) regTxBuilder.addAttachment(ourAttachment) + // DOCEND 28 - // We set the time-window within which the transaction must be notarised using either of: + // There are several ways of setting the transaction's time-window. + // We can set a time-window directly: + // DOCSTART 44 regTxBuilder.setTimeWindow(ourTimeWindow) - regTxBuilder.setTimeWindow(serviceHub.clock.instant(), Duration.ofSeconds(30)) + // DOCEND 44 + // Or as a start time plus a duration (e.g. 45 seconds): + // DOCSTART 45 + regTxBuilder.setTimeWindow(serviceHub.clock.instant(), Duration.ofSeconds(45)) + // DOCEND 45 /**---------------------- * TRANSACTION SIGNING * @@ -299,12 +340,40 @@ object FlowCookbook { // We finalise the transaction by signing it, converting it into a // ``SignedTransaction``. + // DOCSTART 29 val onceSignedTx: SignedTransaction = serviceHub.signInitialTransaction(regTxBuilder) + // DOCEND 29 + // We can also sign the transaction using a different public key: + // DOCSTART 30 + val otherKey: PublicKey = serviceHub.keyManagementService.freshKey() + val onceSignedTx2: SignedTransaction = serviceHub.signInitialTransaction(regTxBuilder, otherKey) + // DOCEND 30 // If instead this was a ``SignedTransaction`` that we'd received // from a counterparty and we needed to sign it, we would add our // signature using: - val twiceSignedTx: SignedTransaction = serviceHub.addSignature(onceSignedTx, dummyPubKey) + // DOCSTART 38 + val twiceSignedTx: SignedTransaction = serviceHub.addSignature(onceSignedTx) + // DOCEND 38 + // Or, if we wanted to use a different public key: + val otherKey2: PublicKey = serviceHub.keyManagementService.freshKey() + // DOCSTART 39 + val twiceSignedTx2: SignedTransaction = serviceHub.addSignature(onceSignedTx, otherKey2) + // DOCEND 39 + + // We can also generate a signature over the transaction without + // adding it to the transaction itself. We may do this when + // sending just the signature in a flow instead of returning the + // entire transaction with our signature. This way, the receiving + // node does not need to check we haven't changed anything in the + // transaction. + // DOCSTART 40 + val sig: DigitalSignature.WithKey = serviceHub.createSignature(onceSignedTx) + // DOCEND 40 + // And again, if we wanted to use a different public key: + // DOCSTART 41 + val sig2: DigitalSignature.WithKey = serviceHub.createSignature(onceSignedTx, otherKey2) + // DOCEND 41 // In practice, however, the process of gathering every signature // but the first can be automated using ``CollectSignaturesFlow``. @@ -329,30 +398,32 @@ object FlowCookbook { subFlow(ResolveTransactionsFlow(setOf(ourStateRef.txhash), counterparty)) // DOCEND 14 - // We verify a transaction using the following one-liner: - twiceSignedTx.tx.toLedgerTransaction(serviceHub).verify() - - // Let's break that down... - // A ``SignedTransaction`` is a pairing of a ``WireTransaction`` // with signatures over this ``WireTransaction``. We don't verify // a signed transaction per se, but rather the ``WireTransaction`` // it contains. + // DOCSTART 31 val wireTx: WireTransaction = twiceSignedTx.tx + // DOCEND 31 // Before we can verify the transaction, we need the // ``ServiceHub`` to use our node's local storage to resolve the // transaction's inputs and attachments into actual objects, // rather than just references. We do this by converting the // ``WireTransaction`` into a ``LedgerTransaction``. + // DOCSTART 32 val ledgerTx: LedgerTransaction = wireTx.toLedgerTransaction(serviceHub) + // DOCEND 32 // We can now verify the transaction. + // DOCSTART 33 ledgerTx.verify() + // DOCEND 33 // We'll often want to perform our own additional verification // too. Just because a transaction is valid based on the contract // rules and requires our signature doesn't mean we have to // sign it! We need to make sure the transaction represents an // agreement we actually want to enter into. + // DOCSTART 34 val outputState: DummyState = wireTx.outputs.single().data as DummyState if (outputState.magicNumber == 777) { // ``FlowException`` is a special exception type. It will be @@ -361,6 +432,7 @@ object FlowCookbook { // failed. throw FlowException("We expected a magic number of 777.") } + // DOCEND 34 // Of course, if you are not a required signer on the transaction, // you have no power to decide whether it is valid or not. If it @@ -381,6 +453,31 @@ object FlowCookbook { val fullySignedTx: SignedTransaction = subFlow(CollectSignaturesFlow(twiceSignedTx, SIGS_GATHERING.childProgressTracker())) // DOCEND 15 + /**----------------------- + * VERIFYING SIGNATURES * + -----------------------**/ + progressTracker.currentStep = VERIFYING_SIGS + + // We can verify that a transaction has all the required + // signatures, and that they're all valid, by running: + // DOCSTART 35 + fullySignedTx.verifySignatures() + // DOCEND 35 + + // If the transaction is only partially signed, we have to pass in + // a list of the public keys corresponding to the missing + // signatures, explicitly telling the system not to check them. + // DOCSTART 36 + onceSignedTx.verifySignatures(counterpartyPubKey) + // DOCEND 36 + + // We can also choose to only check the signatures that are + // present. BE VERY CAREFUL - this function provides no guarantees + // that the signatures are correct, or that none are missing. + // DOCSTART 37 + twiceSignedTx.checkSignaturesAreValid() + // DOCEND 37 + /**----------------------------- * FINALISING THE TRANSACTION * -----------------------------**/ @@ -477,4 +574,4 @@ object FlowCookbook { // we be handled automatically. } } -} \ No newline at end of file +} From 499f1920c7ed95386edfc76f66848ce6a850634b Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Fri, 7 Jul 2017 12:06:28 +0100 Subject: [PATCH 72/97] Simplifies the Hello, World tutorial. --- docs/source/hello-world-contract.rst | 658 ++-------- docs/source/hello-world-flow.rst | 1102 +++-------------- docs/source/hello-world-introduction.rst | 4 +- docs/source/hello-world-running.rst | 47 +- docs/source/hello-world-state.rst | 63 +- docs/source/hello-world-template.rst | 16 +- .../source/resources/simple-tutorial-flow.png | Bin 0 -> 153908 bytes .../resources/simple-tutorial-transaction.png | Bin 0 -> 176926 bytes docs/source/resources/tutorial-state.png | Bin 209230 -> 196921 bytes 9 files changed, 323 insertions(+), 1567 deletions(-) create mode 100644 docs/source/resources/simple-tutorial-flow.png create mode 100644 docs/source/resources/simple-tutorial-transaction.png diff --git a/docs/source/hello-world-contract.rst b/docs/source/hello-world-contract.rst index 3c0ab482f4..de523ddf8b 100644 --- a/docs/source/hello-world-contract.rst +++ b/docs/source/hello-world-contract.rst @@ -18,8 +18,8 @@ It's easy to imagine that most CorDapps will want to impose some constraints on * An asset-trading CorDapp would not want to allow users to finalise a trade without the agreement of their counterparty In Corda, we impose constraints on what transactions are allowed using contracts. These contracts are very different -to the smart contracts of other distributed ledger platforms. They do not represent the current state of the ledger. -Instead, like a real-world contract, they simply impose rules on what kinds of agreements are allowed. +to the smart contracts of other distributed ledger platforms. In Corda, contracts do not represent the current state of +the ledger. Instead, like a real-world contract, they simply impose rules on what kinds of agreements are allowed. Every state is associated with a contract. A transaction is invalid if it does not satisfy the contract of every input and output state in the transaction. @@ -42,11 +42,7 @@ Just as every Corda state must implement the ``ContractState`` interface, every val legalContractReference: SecureHash } -A few more Kotlinisms here: - -* ``fun`` declares a function -* The syntax ``fun funName(arg1Name: arg1Type): returnType`` declares that ``funName`` takes an argument of type - ``arg1Type`` and returns a value of type ``returnType`` +You can read about function declarations in Kotlin `here `_. We can see that ``Contract`` expresses its constraints in two ways: @@ -70,85 +66,121 @@ transfer them or redeem them for cash. One way to enforce this behaviour would b * For the transactions's output IOU state: * Its value must be non-negative - * Its sender and its recipient cannot be the same entity - * All the participants (i.e. both the sender and the recipient) must sign the transaction + * The lender and the borrower cannot be the same entity + * The IOU's borrower must sign the transaction We can picture this transaction as follows: - .. image:: resources/tutorial-transaction.png -:scale: 15% + .. image:: resources/simple-tutorial-transaction.png + :scale: 15% :align: center -Let's write a contract that enforces these constraints. We'll do this by modifying either ``TemplateContract.java`` or -``TemplateContract.kt`` and updating ``TemplateContract`` to define an ``IOUContract``. - Defining IOUContract -------------------- -The Create command -^^^^^^^^^^^^^^^^^^ -The first thing our contract needs is a *command*. Commands serve two purposes: - -* They indicate the transaction's intent, allowing us to perform different verification given the situation - - * For example, a transaction proposing the creation of an IOU could have to satisfy different constraints to one - redeeming an IOU - -* They allow us to define the required signers for the transaction - - * For example, IOU creation might require signatures from both the sender and the recipient, whereas the transfer - of an IOU might only require a signature from the IOUs current holder - -Let's update the definition of ``TemplateContract`` (in ``TemplateContract.java`` or ``TemplateContract.kt``) to -define an ``IOUContract`` with a ``Create`` command: +Let's write a contract that enforces these constraints. We'll do this by modifying either ``TemplateContract.java`` or +``TemplateContract.kt`` and updating ``TemplateContract`` to define an ``IOUContract``: .. container:: codeset .. code-block:: kotlin - package com.template + package com.iou import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash - import net.corda.core.crypto.SecureHash.Companion.sha256 open class IOUContract : Contract { - // Currently, verify() does no checking at all! - override fun verify(tx: TransactionForContract) {} - // Our Create command. class Create : CommandData + override fun verify(tx: TransactionForContract) { + val command = tx.commands.requireSingleCommand() + + requireThat { + // Constraints on the shape of the transaction. + "No inputs should be consumed when issuing an IOU." using (tx.inputs.isEmpty()) + "There should be one output state of type IOUState." using (tx.outputs.size == 1) + + // IOU-specific constraints. + val out = tx.outputs.single() as IOUState + "The IOU's value must be non-negative." using (out.value > 0) + "The lender and the borrower cannot be the same entity." using (out.lender != out.borrower) + + // Constraints on the signers. + "There must only be one signer." using (command.signers.toSet().size == 1) + "The signer must be the borrower." using (command.signers.contains(out.borrower.owningKey)) + } + } + // The legal contract reference - we'll leave this as a dummy hash for now. - override val legalContractReference = SecureHash.sha256("Prose contract.") + override val legalContractReference = SecureHash.zeroHash } .. code-block:: java - package com.template; + package com.iou; + import com.google.common.collect.ImmutableSet; + import net.corda.core.contracts.AuthenticatedObject; import net.corda.core.contracts.CommandData; import net.corda.core.contracts.Contract; + import net.corda.core.contracts.TransactionForContract; import net.corda.core.crypto.SecureHash; + import net.corda.core.identity.Party; + + import static net.corda.core.contracts.ContractsDSL.requireSingleCommand; + import static net.corda.core.contracts.ContractsDSL.requireThat; public class IOUContract implements Contract { - @Override - // Currently, verify() does no checking at all! - public void verify(TransactionForContract tx) {} - // Our Create command. public static class Create implements CommandData {} + @Override + public void verify(TransactionForContract tx) { + final AuthenticatedObject command = requireSingleCommand(tx.getCommands(), Create.class); + + requireThat(check -> { + // Constraints on the shape of the transaction. + check.using("No inputs should be consumed when issuing an IOU.", tx.getInputs().isEmpty()); + check.using("There should be one output state of type IOUState.", tx.getOutputs().size() == 1); + + // IOU-specific constraints. + final IOUState out = (IOUState) tx.getOutputs().get(0); + final Party lender = out.getLender(); + final Party borrower = out.getBorrower(); + check.using("The IOU's value must be non-negative.",out.getValue() > 0); + check.using("The lender and the borrower cannot be the same entity.", lender != borrower); + + // Constraints on the signers. + check.using("There must only be one signer.", ImmutableSet.of(command.getSigners()).size() == 1); + check.using("The signer must be the borrower.", command.getSigners().contains(borrower.getOwningKey())); + + return null; + }); + } + // The legal contract reference - we'll leave this as a dummy hash for now. - private final SecureHash legalContractReference = SecureHash.sha256("Prose contract."); + private final SecureHash legalContractReference = SecureHash.Companion.getZeroHash(); @Override public final SecureHash getLegalContractReference() { return legalContractReference; } } -Aside from renaming ``TemplateContract`` to ``IOUContract``, we've also implemented the ``Create`` command. All -commands must implement the ``CommandData`` interface. +Let's walk through this code step by step. + +The Create command +^^^^^^^^^^^^^^^^^^ +The first thing we add to our contract is a *command*. Commands serve two functions: + +* They indicate the transaction's intent, allowing us to perform different verification given the situation. For + example, a transaction proposing the creation of an IOU could have to satisfy different constraints to one redeeming + an IOU +* They allow us to define the required signers for the transaction. For example, IOU creation might require signatures + from the borrower alone, whereas the transfer of an IOU might require signatures from both the IOU's borrower and lender + +Our contract has one command, a ``Create`` command. All commands must implement the ``CommandData`` interface. The ``CommandData`` interface is a simple marker interface for commands. In fact, its declaration is only two words -long (in Kotlin, interfaces do not require a body): +long (Kotlin interfaces do not require a body): .. container:: codeset @@ -158,8 +190,8 @@ long (in Kotlin, interfaces do not require a body): The verify logic ^^^^^^^^^^^^^^^^ -We now need to define the actual contract constraints. For our IOU CorDapp, we won't concern ourselves with writing -valid legal prose to enforce the IOU agreement in court. Instead, we'll focus on implementing ``verify``. +Our contract also needs to define the actual contract constraints. For our IOU CorDapp, we won't concern ourselves with +writing valid legal prose to enforce the IOU agreement in court. Instead, we'll focus on implementing ``verify``. Remember that our goal in writing the ``verify`` function is to write a function that, given a transaction: @@ -183,84 +215,25 @@ following are true: * The transaction has inputs * The transaction doesn't have exactly one output * The IOU itself is invalid -* The transaction doesn't require signatures from both the sender and the recipient - -Let's work through these constraints one-by-one. +* The transaction doesn't require the borrower's signature Command constraints ~~~~~~~~~~~~~~~~~~~ -To test for the presence of the ``Create`` command, we can use Corda's ``requireSingleCommand`` function: +Our first constraint is around the transaction's commands. We use Corda's ``requireSingleCommand`` function to test for +the presence of a single ``Create`` command. Here, ``requireSingleCommand`` performing a dual purpose: -.. container:: codeset - - .. code-block:: kotlin - - override fun verify(tx: TransactionForContract) { - val command = tx.commands.requireSingleCommand() - } - - .. code-block:: java - - // Additional imports. - import net.corda.core.contracts.AuthenticatedObject; - import net.corda.core.contracts.TransactionForContract; - import static net.corda.core.contracts.ContractsDSL.requireSingleCommand; - - ... - - @Override - public void verify(TransactionForContract tx) { - final AuthenticatedObject command = requireSingleCommand(tx.getCommands(), Create.class); - } - -Here, ``requireSingleCommand`` performing a dual purpose: - -* It's asserting that there is exactly one ``Create`` command in the transaction -* It's extracting the command and returning it +* Asserting that there is exactly one ``Create`` command in the transaction +* Extracting the command and returning it If the ``Create`` command isn't present, or if the transaction has multiple ``Create`` commands, contract verification will fail. Transaction constraints ~~~~~~~~~~~~~~~~~~~~~~~ -We also wanted our transaction to have no inputs and only a single output. One way to impose this constraint is as -follows: +We also want our transaction to have no inputs and only a single output - an issuance transaction. -.. container:: codeset - - .. code-block:: kotlin - - override fun verify(tx: TransactionForContract) { - val command = tx.commands.requireSingleCommand() - - requireThat { - // Constraints on the shape of the transaction. - "No inputs should be consumed when issuing an IOU." using (tx.inputs.isEmpty()) - "Only one output state should be created." using (tx.outputs.size == 1) - } - } - - .. code-block:: java - - // Additional import. - import static net.corda.core.contracts.ContractsDSL.requireThat; - - ... - - @Override - public void verify(TransactionForContract tx) { - final AuthenticatedObject command = requireSingleCommand(tx.getCommands(), Create.class); - - requireThat(check -> { - // Constraints on the shape of the transaction. - check.using("No inputs should be consumed when issuing an IOU.", tx.getInputs().isEmpty()); - check.using("Only one output state should be created.", tx.getOutputs().size() == 1); - - return null; - }); - } - -Note the use of Corda's built-in ``requireThat`` function. ``requireThat`` provides a terse way to write the following: +To impose this and the subsequent constraints, we are using Corda's built-in ``requireThat`` function. ``requireThat`` +provides a terse way to write the following: * If the condition on the right-hand side doesn't evaluate to true... * ...throw an ``IllegalArgumentException`` with the message on the left-hand side @@ -272,457 +245,18 @@ IOU constraints We want to impose two constraints on the ``IOUState`` itself: * Its value must be non-negative -* Its sender and its recipient cannot be the same entity +* The lender and the borrower cannot be the same entity -We can impose these constraints in the same ``requireThat`` block as before: - -.. container:: codeset - - .. code-block:: kotlin - - override fun verify(tx: TransactionForContract) { - val command = tx.commands.requireSingleCommand() - - requireThat { - // Constraints on the shape of the transaction. - "No inputs should be consumed when issuing an IOU." using (tx.inputs.isEmpty()) - "Only one output state should be created." using (tx.outputs.size == 1) - - // IOU-specific constraints. - val out = tx.outputs.single() as IOUState - "The IOU's value must be non-negative." using (out.value > 0) - "The sender and the recipient cannot be the same entity." using (out.sender != out.recipient) - } - } - - .. code-block:: java - - @Override - public void verify(TransactionForContract tx) { - final AuthenticatedObject command = requireSingleCommand(tx.getCommands(), Create.class); - - requireThat(check -> { - // Constraints on the shape of the transaction. - check.using("No inputs should be consumed when issuing an IOU.", tx.getInputs().isEmpty()); - check.using("Only one output state should be created.", tx.getOutputs().size() == 1); - - // IOU-specific constraints. - final IOUState out = (IOUState) tx.getOutputs().get(0); - check.using("The IOU's value must be non-negative.",out.getValue() > 0); - check.using("The sender and the recipient cannot be the same entity.", out.getSender() != out.getRecipient()); - - return null; - }); - } +We impose these constraints in the same ``requireThat`` block as before. You can see that we're not restricted to only writing constraints in the ``requireThat`` block. We can also write other statements - in this case, we're extracting the transaction's single ``IOUState`` and assigning it to a variable. Signer constraints ~~~~~~~~~~~~~~~~~~ -Our final constraint is that the required signers on the transaction are the sender and the recipient only. A -transaction's required signers is equal to the union of all the signers listed on the commands. We can therefore -extract the signers from the ``Create`` command we retrieved earlier. - -.. container:: codeset - - .. code-block:: kotlin - - override fun verify(tx: TransactionForContract) { - val command = tx.commands.requireSingleCommand() - - requireThat { - // Constraints on the shape of the transaction. - "No inputs should be consumed when issuing an IOU." using (tx.inputs.isEmpty()) - "Only one output state should be created." using (tx.outputs.size == 1) - - // IOU-specific constraints. - val out = tx.outputs.single() as IOUState - "The IOU's value must be non-negative." using (out.value > 0) - "The sender and the recipient cannot be the same entity." using (out.sender != out.recipient) - - // Constraints on the signers. - "All of the participants must be signers." using (command.signers.toSet() == out.participants.map { it.owningKey }.toSet()) - } - } - - .. code-block:: java - - // Additional imports. - import com.google.common.collect.ImmutableList; - import java.security.PublicKey; - import java.util.List; - - ... - - @Override - public void verify(TransactionForContract tx) { - final AuthenticatedObject command = requireSingleCommand(tx.getCommands(), Create.class); - - requireThat(check -> { - // Constraints on the shape of the transaction. - check.using("No inputs should be consumed when issuing an IOU.", tx.getInputs().isEmpty()); - check.using("Only one output state should be created.", tx.getOutputs().size() == 1); - - // IOU-specific constraints. - final IOUState out = (IOUState) tx.getOutputs().get(0); - final Party sender = out.getSender(); - final Party recipient = out.getRecipient(); - check.using("The IOU's value must be non-negative.",out.getValue() > 0); - check.using("The sender and the recipient cannot be the same entity.", out.getSender() != out.getRecipient()); - - // Constraints on the signers. - final Set requiredSigners = Sets.newHashSet(sender.getOwningKey(), recipient.getOwningKey()); - final Set signerSet = Sets.newHashSet(command.getSigners()); - check.using("All of the participants must be signers.", (signerSet.equals(requiredSigners))); - - return null; - }); - } - -Checkpoint ----------- -We've now defined the full contract logic of our ``IOUContract``. This contract means that transactions involving -``IOUState`` states will have to fulfill strict constraints to become valid ledger updates. - -Before we move on, let's go back and modify ``IOUState`` to point to the new ``IOUContract``: - -.. container:: codeset - - .. code-block:: kotlin - - class IOUState(val value: Int, - val sender: Party, - val recipient: Party) : ContractState { - override val contract: IOUContract = IOUContract() - - override val participants get() = listOf(sender, recipient) - } - - .. code-block:: java - - public class IOUState implements ContractState { - private final Integer value; - private final Party sender; - private final Party recipient; - private final IOUContract contract = new IOUContract(); - - public IOUState(Integer value, Party sender, Party recipient) { - this.value = value; - this.sender = sender; - this.recipient = recipient; - } - - public Integer getValue() { - return value; - } - - public Party getSender() { - return sender; - } - - public Party getRecipient() { - return recipient; - } - - @Override - public IOUContract getContract() { - return contract; - } - - @Override - public List getParticipants() { - return ImmutableList.of(sender, recipient); - } - } - -Transaction tests ------------------ -How can we ensure that we've defined our contract constraints correctly? - -One option would be to deploy the CorDapp onto a set of nodes, and test it manually. However, this is a relatively -slow process, and would take on the order of minutes to test each change. - -Instead, we can test our contract logic using Corda's ``ledgerDSL`` transaction-testing framework. This will allow us -to test our contract without the overhead of spinning up a set of nodes. - -Open either ``test/kotlin/com/template/contract/ContractTests.kt`` or -``test/java/com/template/contract/ContractTests.java``, and add the following as our first test: - -.. container:: codeset - - .. code-block:: kotlin - - package com.template - - import net.corda.testing.* - import org.junit.Test - - class IOUTransactionTests { - @Test - fun `transaction must include Create command`() { - ledger { - transaction { - output { IOUState(1, MINI_CORP, MEGA_CORP) } - fails() - command(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY) { IOUContract.Create() } - verifies() - } - } - } - } - - .. code-block:: java - - package com.template; - - import net.corda.core.identity.Party; - import org.junit.Test; - import java.security.PublicKey; - import static net.corda.testing.CoreTestUtils.*; - - public class IOUTransactionTests { - static private final Party miniCorp = getMINI_CORP(); - static private final Party megaCorp = getMEGA_CORP(); - static private final PublicKey[] keys = new PublicKey[2]; - - { - keys[0] = getMEGA_CORP_PUBKEY(); - keys[1] = getMINI_CORP_PUBKEY(); - } - - @Test - public void transactionMustIncludeCreateCommand() { - ledger(ledgerDSL -> { - ledgerDSL.transaction(txDSL -> { - txDSL.output(new IOUState(1, miniCorp, megaCorp)); - txDSL.fails(); - txDSL.command(keys, IOUContract.Create::new); - txDSL.verifies(); - return null; - }); - return null; - }); - } - } - -This test uses Corda's built-in ``ledgerDSL`` to: - -* Create a fake transaction -* Add inputs, outputs, commands, etc. (using the DSL's ``output``, ``input`` and ``command`` methods) -* At any point, asserting that the transaction built so far is either contractually valid (by calling ``verifies``) or - contractually invalid (by calling ``fails``) - -In this instance: - -* We initially create a transaction with an output but no command -* We assert that this transaction is invalid (since the ``Create`` command is missing) -* We then add the ``Create`` command -* We assert that transaction is now valid - -Here is the full set of tests we'll be using to test the ``IOUContract``: - -.. container:: codeset - - .. code-block:: kotlin - - class IOUTransactionTests { - @Test - fun `transaction must include Create command`() { - ledger { - transaction { - output { IOUState(1, MINI_CORP, MEGA_CORP) } - fails() - command(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY) { IOUContract.Create() } - verifies() - } - } - } - - @Test - fun `transaction must have no inputs`() { - ledger { - transaction { - input { IOUState(1, MINI_CORP, MEGA_CORP) } - output { IOUState(1, MINI_CORP, MEGA_CORP) } - command(MEGA_CORP_PUBKEY) { IOUContract.Create() } - `fails with`("No inputs should be consumed when issuing an IOU.") - } - } - } - - @Test - fun `transaction must have one output`() { - ledger { - transaction { - output { IOUState(1, MINI_CORP, MEGA_CORP) } - output { IOUState(1, MINI_CORP, MEGA_CORP) } - command(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY) { IOUContract.Create() } - `fails with`("Only one output state should be created.") - } - } - } - - @Test - fun `sender must sign transaction`() { - ledger { - transaction { - output { IOUState(1, MINI_CORP, MEGA_CORP) } - command(MINI_CORP_PUBKEY) { IOUContract.Create() } - `fails with`("All of the participants must be signers.") - } - } - } - - @Test - fun `recipient must sign transaction`() { - ledger { - transaction { - output { IOUState(1, MINI_CORP, MEGA_CORP) } - command(MEGA_CORP_PUBKEY) { IOUContract.Create() } - `fails with`("All of the participants must be signers.") - } - } - } - - @Test - fun `sender is not recipient`() { - ledger { - transaction { - output { IOUState(1, MEGA_CORP, MEGA_CORP) } - command(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY) { IOUContract.Create() } - `fails with`("The sender and the recipient cannot be the same entity.") - } - } - } - - @Test - fun `cannot create negative-value IOUs`() { - ledger { - transaction { - output { IOUState(-1, MINI_CORP, MEGA_CORP) } - command(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY) { IOUContract.Create() } - `fails with`("The IOU's value must be non-negative.") - } - } - } - } - - .. code-block:: java - - public class IOUTransactionTests { - static private final Party miniCorp = getMINI_CORP(); - static private final Party megaCorp = getMEGA_CORP(); - static private final PublicKey[] keys = new PublicKey[2]; - - { - keys[0] = getMEGA_CORP_PUBKEY(); - keys[1] = getMINI_CORP_PUBKEY(); - } - - @Test - public void transactionMustIncludeCreateCommand() { - ledger(ledgerDSL -> { - ledgerDSL.transaction(txDSL -> { - txDSL.output(new IOUState(1, miniCorp, megaCorp)); - txDSL.fails(); - txDSL.command(keys, IOUContract.Create::new); - txDSL.verifies(); - return null; - }); - return null; - }); - } - - @Test - public void transactionMustHaveNoInputs() { - ledger(ledgerDSL -> { - ledgerDSL.transaction(txDSL -> { - txDSL.input(new IOUState(1, miniCorp, megaCorp)); - txDSL.output(new IOUState(1, miniCorp, megaCorp)); - txDSL.command(keys, IOUContract.Create::new); - txDSL.failsWith("No inputs should be consumed when issuing an IOU."); - return null; - }); - return null; - }); - } - - @Test - public void transactionMustHaveOneOutput() { - ledger(ledgerDSL -> { - ledgerDSL.transaction(txDSL -> { - txDSL.output(new IOUState(1, miniCorp, megaCorp)); - txDSL.output(new IOUState(1, miniCorp, megaCorp)); - txDSL.command(keys, IOUContract.Create::new); - txDSL.failsWith("Only one output state should be created."); - return null; - }); - return null; - }); - } - - @Test - public void senderMustSignTransaction() { - ledger(ledgerDSL -> { - ledgerDSL.transaction(txDSL -> { - txDSL.output(new IOUState(1, miniCorp, megaCorp)); - PublicKey[] keys = new PublicKey[1]; - keys[0] = getMINI_CORP_PUBKEY(); - txDSL.command(keys, IOUContract.Create::new); - txDSL.failsWith("All of the participants must be signers."); - return null; - }); - return null; - }); - } - - @Test - public void recipientMustSignTransaction() { - ledger(ledgerDSL -> { - ledgerDSL.transaction(txDSL -> { - txDSL.output(new IOUState(1, miniCorp, megaCorp)); - PublicKey[] keys = new PublicKey[1]; - keys[0] = getMEGA_CORP_PUBKEY(); - txDSL.command(keys, IOUContract.Create::new); - txDSL.failsWith("All of the participants must be signers."); - return null; - }); - return null; - }); - } - - @Test - public void senderIsNotRecipient() { - ledger(ledgerDSL -> { - ledgerDSL.transaction(txDSL -> { - txDSL.output(new IOUState(1, megaCorp, megaCorp)); - PublicKey[] keys = new PublicKey[1]; - keys[0] = getMEGA_CORP_PUBKEY(); - txDSL.command(keys, IOUContract.Create::new); - txDSL.failsWith("The sender and the recipient cannot be the same entity."); - return null; - }); - return null; - }); - } - - @Test - public void cannotCreateNegativeValueIOUs() { - ledger(ledgerDSL -> { - ledgerDSL.transaction(txDSL -> { - txDSL.output(new IOUState(-1, miniCorp, megaCorp)); - txDSL.command(keys, IOUContract.Create::new); - txDSL.failsWith("The IOU's value must be non-negative."); - return null; - }); - return null; - }); - } - } - -Copy these tests into the ContractTests file, and run them to ensure that the ``IOUState`` and ``IOUContract`` are -defined correctly. All the tests should pass. +Finally, we require the borrower's signature on the transaction. A transaction's required signers is equal to the union +of all the signers listed on the commands. We therefore extract the signers from the ``Create`` command we +retrieved earlier. Progress so far --------------- @@ -731,8 +265,10 @@ We've now written an ``IOUContract`` constraining the evolution of each ``IOUSta * An ``IOUState`` can only be created, not transferred or redeemed * Creating an ``IOUState`` requires an issuance transaction with no inputs, a single ``IOUState`` output, and a ``Create`` command -* The ``IOUState`` created by the issuance transaction must have a non-negative value, and its sender and recipient +* The ``IOUState`` created by the issuance transaction must have a non-negative value, and the lender and borrower must be different entities. -The final step in the creation of our CorDapp will be to write the ``IOUFlow`` that will allow nodes to orchestrate -the creation of a new ``IOUState`` on the ledger, while only sharing information on a need-to-know basis. \ No newline at end of file +Before we move on, make sure you go back and modify ``IOUState`` to point to the new ``IOUContract`` class. + +The final step in the creation of our CorDapp will be to write the ``IOUFlow`` that will allow a node to orchestrate +the creation of a new ``IOUState`` on the ledger, while only sharing information on a need-to-know basis. diff --git a/docs/source/hello-world-flow.rst b/docs/source/hello-world-flow.rst index 8b71c7a494..3416be215d 100644 --- a/docs/source/hello-world-flow.rst +++ b/docs/source/hello-world-flow.rst @@ -7,1014 +7,246 @@ Writing the flow ================ A flow describes the sequence of steps for agreeing a specific ledger update. By installing new flows on our node, we -allow the node to handle new business processes. - -We'll have to define two flows to issue an ``IOUState`` onto the ledger: - -* One to be run by the node initiating the creation of the IOU -* One to be run by the node responding to an IOU creation request - -Let's start writing our flows. We'll do this by modifying either ``TemplateFlow.java`` or ``TemplateFlow.kt``. - -FlowLogic ---------- -Each flow is implemented as a ``FlowLogic`` subclass. You define the steps taken by the flow by overriding -``FlowLogic.call``. - -We will define two ``FlowLogic`` instances communicating as a pair. The first will be called ``Initiator``, and will -be run by the sender of the IOU. The other will be called ``Acceptor``, and will be run by the recipient. We group -them together using a class (in Java) or a singleton object (in Kotlin) to show that they are conceptually related. - -Overwrite the existing template code with the following: - -.. container:: codeset - - .. code-block:: kotlin - - package com.template - - 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.Party - import net.corda.core.transactions.SignedTransaction - import net.corda.core.utilities.ProgressTracker - - object IOUFlow { - @InitiatingFlow - @StartableByRPC - class Initiator(val iouValue: Int, - val otherParty: Party): FlowLogic() { - - /** The progress tracker provides checkpoints indicating the progress of the flow to observers. */ - override val progressTracker = ProgressTracker() - - /** The flow logic is encapsulated within the call() method. */ - @Suspendable - override fun call(): SignedTransaction { } - } - - @InitiatedBy(Initiator::class) - class Acceptor(val otherParty: Party) : FlowLogic() { - - @Suspendable - override fun call() { } - } - } - - .. code-block:: java - - package com.template; - - import co.paralleluniverse.fibers.Suspendable; - import net.corda.core.flows.*; - import net.corda.core.identity.Party; - import net.corda.core.transactions.SignedTransaction; - import net.corda.core.utilities.ProgressTracker; - - public class IOUFlow { - @InitiatingFlow - @StartableByRPC - public static class Initiator extends FlowLogic { - private final Integer iouValue; - private final Party otherParty; - - /** The progress tracker provides checkpoints indicating the progress of the flow to observers. */ - private final ProgressTracker progressTracker = new ProgressTracker(); - - public Initiator(Integer iouValue, Party otherParty) { - this.iouValue = iouValue; - this.otherParty = otherParty; - } - - /** The flow logic is encapsulated within the call() method. */ - @Suspendable - @Override - public SignedTransaction call() throws FlowException { } - } - - @InitiatedBy(Initiator.class) - public static class Acceptor extends FlowLogic { - - private final Party otherParty; - - public Acceptor(Party otherParty) { - this.otherParty = otherParty; - } - - @Suspendable - @Override - public Void call() throws FlowException { } - } - } - -We can see that we have two ``FlowLogic`` subclasses, each overriding ``FlowLogic.call``. There's a few things to note: - -* ``FlowLogic.call`` has a return type that matches the type parameter passed to ``FlowLogic`` - this is the return - type of running the flow -* The ``FlowLogic`` subclasses can have constructor parameters, which can be used as arguments to ``FlowLogic.call`` -* ``FlowLogic.call`` is annotated ``@Suspendable`` - this means that the flow will be check-pointed and serialised to - disk when it encounters a long-running operation, allowing your node to move on to running other flows. Forgetting - this annotation out will lead to some very weird error messages -* There are also a few more annotations, on the ``FlowLogic`` subclasses themselves: - - * ``@InitiatingFlow`` means that this flow can be started directly by the node - * ``StartableByRPC`` allows the node owner to start this flow via an RPC call - * ``@InitiatedBy(myClass: Class)`` means that this flow will only start in response to a message sent by another - node running the ``myClass`` flow +allow the node to handle new business processes. Our flow will allow a node to issue an ``IOUState`` onto the ledger. Flow outline ------------ -Now that we've defined our ``FlowLogic`` subclasses, what are the steps we need to take to issue a new IOU onto -the ledger? - -On the initiator side, we need to: +Our flow needs to take the following steps for a borrower to issue a new IOU onto the ledger: 1. Create a valid transaction proposal for the creation of a new IOU 2. Verify the transaction 3. Sign the transaction ourselves - 4. Gather the acceptor's signature - 5. Optionally get the transaction notarised, to: - - * Protect against double-spends for transactions with inputs - * Timestamp transactions that have a ``TimeWindow`` - - 6. Record the transaction in our vault - 7. Send the transaction to the acceptor so that they can record it too - -On the acceptor side, we need to: - - 1. Receive the partially-signed transaction from the initiator - 2. Verify its contents and signatures - 3. Append our signature and send it back to the initiator - 4. Wait to receive back the transaction from the initiator - 5. Record the transaction in our vault + 4. Record the transaction in our vault + 5. Send the transaction to the IOU's lender so that they can record it too Subflows ^^^^^^^^ Although our flow requirements look complex, we can delegate to existing flows to handle many of these tasks. A flow that is invoked within the context of a larger flow to handle a repeatable task is called a *subflow*. -In our initiator flow, we can automate step 4 by invoking ``SignTransactionFlow``, and we can automate steps 5, 6 and -7 using ``FinalityFlow``. Meanwhile, the *entirety* of the acceptor's flow can be automated using -``CollectSignaturesFlow``. +In our initiator flow, we can automate steps 5, 6 and 7 using ``FinalityFlow``. -All we need to do is write the steps to handle the initiator creating and signing the proposed transaction. +All we need to do is write the steps to handle the creation and signing of the proposed transaction. -Writing the initiator's flow ----------------------------- -Let's work through the steps of the initiator's flow one-by-one. +FlowLogic +--------- +Flows are implemented as ``FlowLogic`` subclasses. You define the steps taken by the flow by overriding +``FlowLogic.call``. -Building the transaction -^^^^^^^^^^^^^^^^^^^^^^^^ -We'll approach building the transaction in three steps: - -* Creating a transaction builder -* Creating the transaction's components -* Adding the components to the builder - -TransactionBuilder -~~~~~~~~~~~~~~~~~~ -To start building the proposed transaction, we need a ``TransactionBuilder``. This is a mutable transaction class to -which we can add inputs, outputs, commands, and any other components the transaction needs. - -We create a ``TransactionBuilder`` in ``Initiator.call`` as follows: +We'll write our flow in either ``TemplateFlow.java`` or ``TemplateFlow.kt``. Overwrite the existing template code with +the following: .. container:: codeset .. code-block:: kotlin - // Additional import. + package com.iou + + import co.paralleluniverse.fibers.Suspendable + import net.corda.core.contracts.Command + import net.corda.core.flows.FlowLogic + import net.corda.core.flows.InitiatingFlow + import net.corda.core.flows.StartableByRPC + import net.corda.core.identity.Party import net.corda.core.transactions.TransactionBuilder + import net.corda.core.utilities.ProgressTracker + import net.corda.flows.FinalityFlow - ... + @InitiatingFlow + @StartableByRPC + class IOUFlow(val iouValue: Int, + val otherParty: Party) : FlowLogic() { - @Suspendable - override fun call(): SignedTransaction { - // We create a transaction builder - val txBuilder = TransactionBuilder() - val notaryIdentity = serviceHub.networkMapCache.getAnyNotary() - txBuilder.notary = notaryIdentity + /** The progress tracker provides checkpoints indicating the progress of the flow to observers. */ + override val progressTracker = ProgressTracker() + + /** The flow logic is encapsulated within the call() method. */ + @Suspendable + override fun call() { + // We retrieve the required identities from the network map. + val me = serviceHub.myInfo.legalIdentity + val notary = serviceHub.networkMapCache.getAnyNotary() + + // We create a transaction builder + val txBuilder = TransactionBuilder(notary = notary) + + // We add the items to the builder. + val state = IOUState(iouValue, me, otherParty) + val cmd = Command(IOUContract.Create(), me.owningKey) + txBuilder.withItems(state, cmd) + + // Verifying the transaction. + txBuilder.verify(serviceHub) + + // Signing the transaction. + val signedTx = serviceHub.signInitialTransaction(txBuilder) + + // Finalising the transaction. + subFlow(FinalityFlow(signedTx)) + } } .. code-block:: java - // Additional import. + package com.iou; + + import co.paralleluniverse.fibers.Suspendable; + import net.corda.core.contracts.Command; + import net.corda.core.flows.FlowException; + import net.corda.core.flows.FlowLogic; + import net.corda.core.flows.InitiatingFlow; + import net.corda.core.flows.StartableByRPC; + 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 net.corda.flows.FinalityFlow; - ... + @InitiatingFlow + @StartableByRPC + public class IOUFlow extends FlowLogic { + private final Integer iouValue; + private final Party otherParty; - @Suspendable - @Override - public SignedTransaction call() throws FlowException { - // We create a transaction builder - final TransactionBuilder txBuilder = new TransactionBuilder(); - final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null); - txBuilder.setNotary(notary); + /** + * The progress tracker provides checkpoints indicating the progress of the flow to observers. + */ + private final ProgressTracker progressTracker = new ProgressTracker(); + + public IOUFlow(Integer iouValue, Party otherParty) { + this.iouValue = iouValue; + this.otherParty = otherParty; + } + + /** + * The flow logic is encapsulated within the call() method. + */ + @Suspendable + @Override + public Void call() throws FlowException { + // We retrieve the required identities from the network map. + final Party me = getServiceHub().getMyInfo().getLegalIdentity(); + final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null); + + // We create a transaction builder + final TransactionBuilder txBuilder = new TransactionBuilder(); + txBuilder.setNotary(notary); + + // We add the items to the builder. + IOUState state = new IOUState(iouValue, me, otherParty); + Command cmd = new Command(new IOUContract.Create(), me.getOwningKey()); + txBuilder.withItems(state, cmd); + + // Verifying the transaction. + txBuilder.verify(getServiceHub()); + + // Signing the transaction. + final SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder); + + // Finalising the transaction. + subFlow(new FinalityFlow(signedTx)); + + return null; + } } -In the first line, we create a ``TransactionBuilder``. We will also want our transaction to have a notary, in order -to prevent double-spends. In the second line, we retrieve the identity of the notary who will be notarising our -transaction and add it to the builder. +We now have our own ``FlowLogic`` subclass that overrides ``FlowLogic.call``. There's a few things to note: + +* ``FlowLogic.call`` has a return type that matches the type parameter passed to ``FlowLogic`` - this is type returned + by running the flow +* ``FlowLogic`` subclasses can have constructor parameters, which can be used as arguments to ``FlowLogic.call`` +* ``FlowLogic.call`` is annotated ``@Suspendable`` - this means that the flow will be check-pointed and serialised to + disk when it encounters a long-running operation, allowing your node to move on to running other flows. Forgetting + this annotation out will lead to some very weird error messages +* There are also a few more annotations, on the ``FlowLogic`` subclass itself: + + * ``@InitiatingFlow`` means that this flow can be started directly by the node + * ``StartableByRPC`` allows the node owner to start this flow via an RPC call + +Let's walk through the steps of ``FlowLogic.call`` one-by-one: + +Retrieving participant information +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +The identity of our counterparty is passed in as a constructor argument. However, we need to use the ``ServiceHub`` to +retrieve our identity, as well as the identity of the notary we'll be using for our transaction. You can see that the notary's identity is being retrieved from the node's ``ServiceHub``. Whenever we need information within a flow - whether it's about our own node, its contents, or the rest of the network - we use the node's ``ServiceHub``. In particular, ``ServiceHub.networkMapCache`` provides information about the other nodes on the network and the services that they offer. -Transaction components -~~~~~~~~~~~~~~~~~~~~~~ -Now that we have our ``TransactionBuilder``, we need to create its components. Remember that we're trying to build +Building the transaction +^^^^^^^^^^^^^^^^^^^^^^^^ +We'll build our transaction proposal in two steps: + +* Creating a transaction builder +* Adding the desired items to the builder + +Creating a transaction builder +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +To start building the proposed transaction, we need a ``TransactionBuilder``. This is a mutable transaction class to +which we can add inputs, outputs, commands, and any other items the transaction needs. We create a +``TransactionBuilder`` that uses the notary we retrieved earlier. + +Transaction items +~~~~~~~~~~~~~~~~~ +Now that we have our ``TransactionBuilder``, we need to add the desired items. Remember that we're trying to build the following transaction: - .. image:: resources/tutorial-transaction.png -:scale: 15% + .. image:: resources/simple-tutorial-transaction.png + :scale: 15% :align: center So we'll need the following: * The output ``IOUState`` -* A ``Create`` command listing both the IOU's sender and recipient as signers +* A ``Create`` command listing the IOU's borrower as a signer -We create these components as follows: +The command we use pairs the ``IOUContract.Create`` command defined earlier with our public key. Including this command +in the transaction makes us one of the transaction's required signers. -.. container:: codeset +We add these items to the transaction using the ``TransactionBuilder.withItems`` method, which takes a ``vararg`` of: - .. code-block:: kotlin - - // Additional import. - import net.corda.core.contracts.Command - - ... - - @Suspendable - override fun call(): SignedTransaction { - // We create a transaction builder - val txBuilder = TransactionBuilder() - val notaryIdentity = serviceHub.networkMapCache.getAnyNotary() - txBuilder.notary = notaryIdentity - - // We create the transaction's components. - val ourIdentity = serviceHub.myInfo.legalIdentity - val iou = IOUState(iouValue, ourIdentity, otherParty) - val txCommand = Command(IOUContract.Create(), iou.participants.map { it.owningKey }) - } - - .. code-block:: java - - // Additional imports. - import com.google.common.collect.ImmutableList; - import net.corda.core.contracts.Command; - import java.security.PublicKey; - import java.util.List; - - ... - - @Suspendable - @Override - public SignedTransaction call() throws FlowException { - // We create a transaction builder - final TransactionBuilder txBuilder = new TransactionBuilder(); - final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null); - txBuilder.setNotary(notary); - - // We create the transaction's components. - final Party ourIdentity = getServiceHub().getMyInfo().getLegalIdentity(); - final IOUState iou = new IOUState(iouValue, ourIdentity, otherParty); - final List signers = ImmutableList.of(ourIdentity.getOwningKey(), otherParty.getOwningKey()); - final Command txCommand = new Command(new IOUContract.Create(), signers); - } - -To build the state, we start by retrieving our own identity (again, we get this information from the ``ServiceHub``, -via ``ServiceHub.myInfo``). We then build the ``IOUState``, using our identity, the ``IOUContract``, and the IOU -value and counterparty from the ``FlowLogic``'s constructor parameters. - -We also create the command, which pairs the ``IOUContract.Create`` command with the public keys of ourselves and the -counterparty. If this command is included in the transaction, both ourselves and the counterparty will be required -signers. - -Adding the components -~~~~~~~~~~~~~~~~~~~~~ -Finally, we add the items to the transaction using the ``TransactionBuilder.withItems`` method: - -.. container:: codeset - - .. code-block:: kotlin - - @Suspendable - override fun call(): SignedTransaction { - // We create a transaction builder - val txBuilder = TransactionBuilder() - val notaryIdentity = serviceHub.networkMapCache.getAnyNotary() - txBuilder.notary = notaryIdentity - - // We create the transaction's components. - val ourIdentity = serviceHub.myInfo.legalIdentity - val iou = IOUState(iouValue, ourIdentity, otherParty) - val txCommand = Command(IOUContract.Create(), iou.participants.map { it.owningKey }) - - // Adding the item's to the builder. - txBuilder.withItems(iou, txCommand) - } - - .. code-block:: java - - @Suspendable - @Override - public SignedTransaction call() throws FlowException { - // We create a transaction builder - final TransactionBuilder txBuilder = new TransactionBuilder(); - final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null); - txBuilder.setNotary(notary); - - // We create the transaction's components. - final Party ourIdentity = getServiceHub().getMyInfo().getLegalIdentity(); - final IOUState iou = new IOUState(iouValue, ourIdentity, otherParty); - final List signers = ImmutableList.of(ourIdentity.getOwningKey(), otherParty.getOwningKey()); - final Command txCommand = new Command(new IOUContract.Create(), signers); - - // Adding the item's to the builder. - txBuilder.withItems(iou, txCommand); - } - -``TransactionBuilder.withItems`` takes a `vararg` of: - -* `ContractState` objects, which are added to the builder as output states -* `StateRef` objects (references to the outputs of previous transactions), which are added to the builder as input +* ``ContractState`` or ``TransactionState`` objects, which are added to the builder as output states +* ``StateRef`` objects (references to the outputs of previous transactions), which are added to the builder as input state references -* `Command` objects, which are added to the builder as commands +* ``Command`` objects, which are added to the builder as commands +* ``SecureHash`` objects, which are added to the builder as attachments +* ``TimeWindow`` objects, which set the time-window of the transaction It will modify the ``TransactionBuilder`` in-place to add these components to it. Verifying the transaction ^^^^^^^^^^^^^^^^^^^^^^^^^ We've now built our proposed transaction. Before we sign it, we should check that it represents a valid ledger update -proposal by verifying the transaction, which will execute each of the transaction's contracts: - -.. container:: codeset - - .. code-block:: kotlin - - @Suspendable - override fun call(): SignedTransaction { - // We create a transaction builder - val txBuilder = TransactionBuilder() - val notaryIdentity = serviceHub.networkMapCache.getAnyNotary() - txBuilder.notary = notaryIdentity - - // We create the transaction's components. - val ourIdentity = serviceHub.myInfo.legalIdentity - val iou = IOUState(iouValue, ourIdentity, otherParty) - val txCommand = Command(IOUContract.Create(), iou.participants.map { it.owningKey }) - - // Adding the item's to the builder. - txBuilder.withItems(iou, txCommand) - - // Verifying the transaction. - txBuilder.toWireTransaction().toLedgerTransaction(serviceHub).verify() - } - - .. code-block:: java - - @Suspendable - @Override - public SignedTransaction call() throws FlowException { - // We create a transaction builder - final TransactionBuilder txBuilder = new TransactionBuilder(); - final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null); - txBuilder.setNotary(notary); - - // We create the transaction's components. - final Party ourIdentity = getServiceHub().getMyInfo().getLegalIdentity(); - final IOUState iou = new IOUState(iouValue, ourIdentity, otherParty); - final List signers = ImmutableList.of(ourIdentity.getOwningKey(), otherParty.getOwningKey()); - final Command txCommand = new Command(new IOUContract.Create(), signers); - - // Adding the item's to the builder. - txBuilder.withItems(iou, txCommand); - - // Verifying the transaction. - txBuilder.toWireTransaction().toLedgerTransaction(getServiceHub()).verify(); - } - -To verify the transaction, we must: - -* Convert the builder into an immutable ``WireTransaction`` -* Convert the ``WireTransaction`` into a ``LedgerTransaction`` using the ``ServiceHub``. This step resolves the - transaction's input state references and attachment references into actual states and attachments (in case their - contents are needed to verify the transaction -* Call ``LedgerTransaction.verify`` to test whether the transaction is valid based on the contract of every input and - output state in the transaction +proposal by verifying the transaction, which will execute each of the transaction's contracts. If the verification fails, we have built an invalid transaction. Our flow will then end, throwing a ``TransactionVerificationException``. Signing the transaction ^^^^^^^^^^^^^^^^^^^^^^^ -Now that we are satisfied that our transaction proposal is valid, we sign it. Once the transaction is signed, -no-one will be able to modify the transaction without invalidating our signature. This effectively makes the -transaction immutable. - -.. container:: codeset - - .. code-block:: kotlin - - @Suspendable - override fun call(): SignedTransaction { - // We create a transaction builder - val txBuilder = TransactionBuilder() - val notaryIdentity = serviceHub.networkMapCache.getAnyNotary() - txBuilder.notary = notaryIdentity - - // We create the transaction's components. - val ourIdentity = serviceHub.myInfo.legalIdentity - val iou = IOUState(iouValue, ourIdentity, otherParty) - val txCommand = Command(IOUContract.Create(), iou.participants.map { it.owningKey }) - - // Adding the item's to the builder. - txBuilder.withItems(iou, txCommand) - - // Verifying the transaction. - txBuilder.toWireTransaction().toLedgerTransaction(serviceHub).verify() - - // Signing the transaction. - val partSignedTx = serviceHub.signInitialTransaction(txBuilder) - } - - .. code-block:: java - - @Suspendable - @Override - public SignedTransaction call() throws FlowException { - // We create a transaction builder - final TransactionBuilder txBuilder = new TransactionBuilder(); - final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null); - txBuilder.setNotary(notary); - - // We create the transaction's components. - final Party ourIdentity = getServiceHub().getMyInfo().getLegalIdentity(); - final IOUState iou = new IOUState(iouValue, ourIdentity, otherParty); - final List signers = ImmutableList.of(ourIdentity.getOwningKey(), otherParty.getOwningKey()); - final Command txCommand = new Command(new IOUContract.Create(), signers); - - // Adding the item's to the builder. - txBuilder.withItems(iou, txCommand); - - // Verifying the transaction. - txBuilder.toWireTransaction().toLedgerTransaction(getServiceHub()).verify(); - - // Signing the transaction. - final SignedTransaction partSignedTx = getServiceHub().signInitialTransaction(txBuilder); - } +Now that we have a valid transaction proposal, we need to sign it. Once the transaction is signed, no-one will be able +to modify the transaction without invalidating our signature, effectively making the transaction immutable. The call to ``ServiceHub.signInitialTransaction`` returns a ``SignedTransaction`` - an object that pairs the transaction itself with a list of signatures over that transaction. -We can now safely send the builder to our counterparty. If the counterparty tries to modify the transaction, the -transaction's hash will change, our digital signature will no longer be valid, and the transaction will not be accepted -as a valid ledger update. - -Gathering counterparty signatures -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The final step in order to create a valid transaction proposal is to collect the counterparty's signature. As -discussed, we can automate this process by invoking the built-in ``CollectSignaturesFlow``: - -.. container:: codeset - - .. code-block:: kotlin - - // Additional import. - import net.corda.flows.CollectSignaturesFlow - - ... - - @Suspendable - override fun call(): SignedTransaction { - // We create a transaction builder - val txBuilder = TransactionBuilder() - val notaryIdentity = serviceHub.networkMapCache.getAnyNotary() - txBuilder.notary = notaryIdentity - - // We create the transaction's components. - val ourIdentity = serviceHub.myInfo.legalIdentity - val iou = IOUState(iouValue, ourIdentity, otherParty) - val txCommand = Command(IOUContract.Create(), iou.participants.map { it.owningKey }) - - // Adding the item's to the builder. - txBuilder.withItems(iou, txCommand) - - // Verifying the transaction. - txBuilder.toWireTransaction().toLedgerTransaction(serviceHub).verify() - - // Signing the transaction. - val partSignedTx = serviceHub.signInitialTransaction(txBuilder) - - // Gathering the signatures. - val signedTx = subFlow(CollectSignaturesFlow(partSignedTx, CollectSignaturesFlow.tracker())) - } - - .. code-block:: java - - // Additional import. - import net.corda.flows.CollectSignaturesFlow; - - ... - - @Suspendable - @Override - public SignedTransaction call() throws FlowException { - // We create a transaction builder - final TransactionBuilder txBuilder = new TransactionBuilder(); - final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null); - txBuilder.setNotary(notary); - - // We create the transaction's components. - final Party ourIdentity = getServiceHub().getMyInfo().getLegalIdentity(); - final IOUState iou = new IOUState(iouValue, ourIdentity, otherParty); - final List signers = ImmutableList.of(ourIdentity.getOwningKey(), otherParty.getOwningKey()); - final Command txCommand = new Command(new IOUContract.Create(), signers); - - // Adding the item's to the builder. - txBuilder.withItems(iou, txCommand); - - // Verifying the transaction. - txBuilder.toWireTransaction().toLedgerTransaction(getServiceHub()).verify(); - - // Signing the transaction. - final SignedTransaction partSignedTx = getServiceHub().signInitialTransaction(txBuilder); - - // Gathering the signatures. - final SignedTransaction signedTx = subFlow( - new CollectSignaturesFlow(partSignedTx, CollectSignaturesFlow.Companion.tracker())); - } - -``CollectSignaturesFlow`` gathers signatures from every participant listed on the transaction, and returns a -``SignedTransaction`` with all the required signatures. - Finalising the transaction ^^^^^^^^^^^^^^^^^^^^^^^^^^ -We now have a valid transaction signed by all the required parties. All that's left to do is to have it notarised and -recorded by all the relevant parties. From then on, it will become a permanent part of the ledger. Again, instead -of handling this process manually, we'll use a built-in flow called ``FinalityFlow``: - -.. container:: codeset - - .. code-block:: kotlin - - // Additional import. - import net.corda.flows.FinalityFlow - - ... - - @Suspendable - override fun call(): SignedTransaction { - // We create a transaction builder - val txBuilder = TransactionBuilder() - val notaryIdentity = serviceHub.networkMapCache.getAnyNotary() - txBuilder.notary = notaryIdentity - - // We create the transaction's components. - val ourIdentity = serviceHub.myInfo.legalIdentity - val iou = IOUState(iouValue, ourIdentity, otherParty) - val txCommand = Command(IOUContract.Create(), iou.participants.map { it.owningKey }) - - // Adding the item's to the builder. - txBuilder.withItems(iou, txCommand) - - // Verifying the transaction. - txBuilder.toWireTransaction().toLedgerTransaction(serviceHub).verify() - - // Signing the transaction. - val partSignedTx = serviceHub.signInitialTransaction(txBuilder) - - // Gathering the signatures. - val signedTx = subFlow(CollectSignaturesFlow(partSignedTx, CollectSignaturesFlow.tracker())) - - // Finalising the transaction. - return subFlow(FinalityFlow(signedTx)).single() - } - - .. code-block:: java - - // Additional import. - import net.corda.flows.FinalityFlow; - - ... - - @Suspendable - @Override - public SignedTransaction call() throws FlowException { - // We create a transaction builder - final TransactionBuilder txBuilder = new TransactionBuilder(); - final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null); - txBuilder.setNotary(notary); - - // We create the transaction's components. - final Party ourIdentity = getServiceHub().getMyInfo().getLegalIdentity(); - final IOUState iou = new IOUState(iouValue, ourIdentity, otherParty); - final List signers = ImmutableList.of(ourIdentity.getOwningKey(), otherParty.getOwningKey()); - final Command txCommand = new Command(new IOUContract.Create(), signers); - - // Adding the item's to the builder. - txBuilder.withItems(iou, txCommand); - - // Verifying the transaction. - txBuilder.toWireTransaction().toLedgerTransaction(getServiceHub()).verify(); - - // Signing the transaction. - final SignedTransaction partSignedTx = getServiceHub().signInitialTransaction(txBuilder); - - // Gathering the signatures. - final SignedTransaction signedTx = subFlow( - new CollectSignaturesFlow(partSignedTx, CollectSignaturesFlow.Companion.tracker())); - - // Finalising the transaction. - return subFlow(new FinalityFlow(signedTx)).get(0); - } +Now that we have a valid signed transaction, all that's left to do is to have it notarised and recorded by all the +relevant parties. By doing so, it will become a permanent part of the ledger. As discussed, we'll handle this process +automatically using a built-in flow called ``FinalityFlow``: ``FinalityFlow`` completely automates the process of: -* Notarising the transaction +* Notarising the transaction if required (i.e. if the transaction contains inputs and/or a time-window) * Recording it in our vault -* Sending it to the counterparty for them to record as well +* Sending it to the other participants (i.e. the lender) for them to record as well -``FinalityFlow`` also returns a list of the notarised transactions. We extract the single item from this list and -return it. - -That completes the initiator side of the flow. - -Writing the acceptor's flow ---------------------------- -The acceptor's side of the flow is much simpler. We need to: - -1. Receive a signed transaction from the counterparty -2. Verify the transaction -3. Sign the transaction -4. Send the updated transaction back to the counterparty - -As we just saw, the process of building and finalising the transaction will be completely handled by the initiator flow. - -SignTransactionFlow -~~~~~~~~~~~~~~~~~~~ -We can automate all four steps of the acceptor's flow by invoking ``SignTransactionFlow``. ``SignTransactionFlow`` is -a flow that is registered by default on every node to respond to messages from ``CollectSignaturesFlow`` (which is -invoked by the initiator flow). - -As ``SignTransactionFlow`` is an abstract class, we have to subclass it and override -``SignTransactionFlow.checkTransaction``: - -.. container:: codeset - - .. code-block:: kotlin - - // Additional import. - import net.corda.flows.SignTransactionFlow - - ... - - @InitiatedBy(Initiator::class) - class Acceptor(val otherParty: Party) : FlowLogic() { - - @Suspendable - override fun call() { - // Stage 1 - Verifying and signing the transaction. - subFlow(object : SignTransactionFlow(otherParty, tracker()) { - override fun checkTransaction(stx: SignedTransaction) { - // Define custom verification logic here. - } - }) - } - } - - .. code-block:: java - - // Additional import. - import net.corda.flows.SignTransactionFlow; - - ... - - @InitiatedBy(Initiator.class) - public static class Acceptor extends FlowLogic { - - private final Party otherParty; - - public Acceptor(Party otherParty) { - this.otherParty = otherParty; - } - - @Suspendable - @Override - public Void call() throws FlowException { - // Stage 1 - Verifying and signing the transaction. - - class signTxFlow extends SignTransactionFlow { - private signTxFlow(Party otherParty, ProgressTracker progressTracker) { - super(otherParty, progressTracker); - } - - @Override - protected void checkTransaction(SignedTransaction signedTransaction) { - // Define custom verification logic here. - } - } - - subFlow(new signTxFlow(otherParty, SignTransactionFlow.Companion.tracker())); - - return null; - } - } - -``SignTransactionFlow`` already checks the transaction's signatures, and whether the transaction is contractually -valid. The purpose of ``SignTransactionFlow.checkTransaction`` is to define any additional verification of the -transaction that we wish to perform before we sign it. For example, we may want to: - -* Check that the transaction contains an ``IOUState`` -* Check that the IOU's value isn't too high - -Well done! You've finished the flows! - -Flow tests ----------- -As with contracts, deploying nodes to manually test flows is not efficient. Instead, we can use Corda's flow-test -DSL to quickly test our flows. The flow-test DSL works by creating a network of lightweight, "mock" node -implementations on which we run our flows. - -The first thing we need to do is create this mock network. Open either ``test/kotlin/com/template/flow/FlowTests.kt`` or -``test/java/com/template/contract/ContractTests.java``, and overwrite the existing code with: - -.. container:: codeset - - .. code-block:: kotlin - - package com.template - - import net.corda.core.contracts.TransactionVerificationException - import net.corda.core.getOrThrow - import net.corda.testing.node.MockNetwork - import net.corda.testing.node.MockNetwork.MockNode - import org.junit.After - import org.junit.Before - import org.junit.Test - import kotlin.test.assertEquals - import kotlin.test.assertFailsWith - - class IOUFlowTests { - lateinit var net: MockNetwork - lateinit var a: MockNode - lateinit var b: MockNode - lateinit var c: MockNode - - @Before - fun setup() { - net = MockNetwork() - val nodes = net.createSomeNodes(2) - a = nodes.partyNodes[0] - b = nodes.partyNodes[1] - b.registerInitiatedFlow(IOUFlow.Acceptor::class.java) - net.runNetwork() - } - - @After - fun tearDown() { - net.stopNodes() - } - } - - .. code-block:: java - - package com.template; - - import com.google.common.collect.ImmutableList; - import com.google.common.util.concurrent.ListenableFuture; - import net.corda.core.contracts.ContractState; - import net.corda.core.contracts.TransactionState; - import net.corda.core.contracts.TransactionVerificationException; - import net.corda.core.transactions.SignedTransaction; - import net.corda.testing.node.MockNetwork; - import net.corda.testing.node.MockNetwork.BasketOfNodes; - import net.corda.testing.node.MockNetwork.MockNode; - import org.junit.After; - import org.junit.Before; - import org.junit.Rule; - import org.junit.Test; - import org.junit.rules.ExpectedException; - - import java.util.List; - - import static org.hamcrest.CoreMatchers.instanceOf; - import static org.junit.Assert.assertEquals; - - public class IOUFlowTests { - private MockNetwork net; - private MockNode a; - private MockNode b; - - @Before - public void setup() { - net = new MockNetwork(); - BasketOfNodes nodes = net.createSomeNodes(2); - a = nodes.getPartyNodes().get(0); - b = nodes.getPartyNodes().get(1); - b.registerInitiatedFlow(IOUFlow.Acceptor.class); - net.runNetwork(); - } - - @After - public void tearDown() { - net.stopNodes(); - } - - @Rule - public final ExpectedException exception = ExpectedException.none(); - } - -This creates an in-memory network with mocked-out components. The network has two nodes, plus network map and notary -nodes. We register any responder flows (in our case, ``IOUFlow.Acceptor``) on our nodes as well. - -Our first test will be to check that the flow rejects invalid IOUs: - -.. container:: codeset - - .. code-block:: kotlin - - @Test - fun `flow rejects invalid IOUs`() { - val flow = IOUFlow.Initiator(-1, b.info.legalIdentity) - val future = a.services.startFlow(flow).resultFuture - net.runNetwork() - - // The IOUContract specifies that IOUs cannot have negative values. - assertFailsWith {future.getOrThrow()} - } - - .. code-block:: java - - @Test - public void flowRejectsInvalidIOUs() throws Exception { - IOUFlow.Initiator flow = new IOUFlow.Initiator(-1, b.info.getLegalIdentity()); - ListenableFuture future = a.getServices().startFlow(flow).getResultFuture(); - net.runNetwork(); - - exception.expectCause(instanceOf(TransactionVerificationException.class)); - future.get(); - } - -This code causes node A to run the ``IOUFlow.Initiator`` flow. The call to ``MockNetwork.runNetwork`` is required to -simulate the running of a real network. - -We then assert that because we passed in a negative IOU value to the flow's constructor, the flow should fail with a -``TransactionVerificationException``. In other words, we are asserting that at some point in flow, the transaction is -verified (remember that ``IOUContract`` forbids negative value IOUs), causing the flow to fail. - -Because flows need to be instrumented by a library called `Quasar `_ that -allows the flows to be checkpointed and serialized to disk, you need to run these tests using the provided -``Run Flow Tests - Java`` or ``Run Flow Tests - Kotlin`` run-configurations. - -Here is the full suite of tests we'll use for the ``IOUFlow``: - -.. container:: codeset - - .. code-block:: kotlin - - @Test - fun `flow rejects invalid IOUs`() { - val flow = IOUFlow.Initiator(-1, b.info.legalIdentity) - val future = a.services.startFlow(flow).resultFuture - net.runNetwork() - - // The IOUContract specifies that IOUs cannot have negative values. - assertFailsWith {future.getOrThrow()} - } - - @Test - fun `SignedTransaction returned by the flow is signed by the initiator`() { - val flow = IOUFlow.Initiator(1, b.info.legalIdentity) - val future = a.services.startFlow(flow).resultFuture - net.runNetwork() - - val signedTx = future.getOrThrow() - signedTx.verifySignatures(b.services.legalIdentityKey) - } - - @Test - fun `SignedTransaction returned by the flow is signed by the acceptor`() { - val flow = IOUFlow.Initiator(1, b.info.legalIdentity) - val future = a.services.startFlow(flow).resultFuture - net.runNetwork() - - val signedTx = future.getOrThrow() - signedTx.verifySignatures(a.services.legalIdentityKey) - } - - @Test - fun `flow records a transaction in both parties' vaults`() { - val flow = IOUFlow.Initiator(1, b.info.legalIdentity) - val future = a.services.startFlow(flow).resultFuture - net.runNetwork() - val signedTx = future.getOrThrow() - - // We check the recorded transaction in both vaults. - for (node in listOf(a, b)) { - assertEquals(signedTx, node.storage.validatedTransactions.getTransaction(signedTx.id)) - } - } - - @Test - fun `recorded transaction has no inputs and a single output, the input IOU`() { - val flow = IOUFlow.Initiator(1, b.info.legalIdentity) - val future = a.services.startFlow(flow).resultFuture - net.runNetwork() - val signedTx = future.getOrThrow() - - // We check the recorded transaction in both vaults. - for (node in listOf(a, b)) { - val recordedTx = node.storage.validatedTransactions.getTransaction(signedTx.id) - val txOutputs = recordedTx!!.tx.outputs - assert(txOutputs.size == 1) - - val recordedState = txOutputs[0].data as IOUState - assertEquals(recordedState.value, 1) - assertEquals(recordedState.sender, a.info.legalIdentity) - assertEquals(recordedState.recipient, b.info.legalIdentity) - } - } - - .. code-block:: java - - @Test - public void flowRejectsInvalidIOUs() throws Exception { - IOUFlow.Initiator flow = new IOUFlow.Initiator(-1, b.info.getLegalIdentity()); - ListenableFuture future = a.getServices().startFlow(flow).getResultFuture(); - net.runNetwork(); - - exception.expectCause(instanceOf(TransactionVerificationException.class)); - future.get(); - } - - @Test - public void signedTransactionReturnedByTheFlowIsSignedByTheInitiator() throws Exception { - IOUFlow.Initiator flow = new IOUFlow.Initiator(1, b.info.getLegalIdentity()); - ListenableFuture future = a.getServices().startFlow(flow).getResultFuture(); - net.runNetwork(); - - SignedTransaction signedTx = future.get(); - signedTx.verifySignatures(b.getServices().getLegalIdentityKey()); - } - - @Test - public void signedTransactionReturnedByTheFlowIsSignedByTheAcceptor() throws Exception { - IOUFlow.Initiator flow = new IOUFlow.Initiator(1, b.info.getLegalIdentity()); - ListenableFuture future = a.getServices().startFlow(flow).getResultFuture(); - net.runNetwork(); - - SignedTransaction signedTx = future.get(); - signedTx.verifySignatures(a.getServices().getLegalIdentityKey()); - } - - @Test - public void flowRecordsATransactionInBothPartiesVaults() throws Exception { - IOUFlow.Initiator flow = new IOUFlow.Initiator(1, b.info.getLegalIdentity()); - ListenableFuture future = a.getServices().startFlow(flow).getResultFuture(); - net.runNetwork(); - SignedTransaction signedTx = future.get(); - - for (MockNode node : ImmutableList.of(a, b)) { - assertEquals(signedTx, node.storage.getValidatedTransactions().getTransaction(signedTx.getId())); - } - } - - @Test - public void recordedTransactionHasNoInputsAndASingleOutputTheInputIOU() throws Exception { - IOUFlow.Initiator flow = new IOUFlow.Initiator(1, b.info.getLegalIdentity()); - ListenableFuture future = a.getServices().startFlow(flow).getResultFuture(); - net.runNetwork(); - SignedTransaction signedTx = future.get(); - - for (MockNode node : ImmutableList.of(a, b)) { - SignedTransaction recordedTx = node.storage.getValidatedTransactions().getTransaction(signedTx.getId()); - List> txOutputs = recordedTx.getTx().getOutputs(); - assert(txOutputs.size() == 1); - - IOUState recordedState = (IOUState) txOutputs.get(0).getData(); - assert(recordedState.getValue() == 1); - assertEquals(recordedState.getSender(), a.info.getLegalIdentity()); - assertEquals(recordedState.getRecipient(), b.info.getLegalIdentity()); - } - } - -Run these tests and make sure they all pass. If they do, its very likely that we have a working CorDapp. +Our flow, and our CorDapp, are now ready! Progress so far --------------- -We now have a flow that we can kick off on our node to completely automate the process of issuing an IOU onto the -ledger. Under the hood, this flow takes the form of two communicating ``FlowLogic`` subclasses. - -We now have a complete CorDapp, made up of: - -* The ``IOUState``, representing IOUs on the ledger -* The ``IOUContract``, controlling the evolution of ``IOUState`` objects over time -* The ``IOUFlow``, which transforms the creation of a new IOU on the ledger into a push-button process - -The final step is to spin up some nodes and test our CorDapp. \ No newline at end of file +We have now defined a flow that we can start on our node to completely automate the process of issuing an IOU onto the +ledger. The final step is to spin up some nodes and test our CorDapp. \ No newline at end of file diff --git a/docs/source/hello-world-introduction.rst b/docs/source/hello-world-introduction.rst index d66ffeb272..9760d91a6d 100644 --- a/docs/source/hello-world-introduction.rst +++ b/docs/source/hello-world-introduction.rst @@ -44,10 +44,10 @@ However, we can easily extend our CorDapp to handle additional use-cases later o Flow ^^^^ -Our flow will be the IOUFlow. It will allow two nodes to orchestrate the creation of a new IOU on the ledger, via the +Our flow will be the IOUFlow. It will allow a node to orchestrate the creation of a new IOU on the ledger, via the following steps: - .. image:: resources/tutorial-flow.png + .. image:: resources/simple-tutorial-flow.png :scale: 25% :align: center diff --git a/docs/source/hello-world-running.rst b/docs/source/hello-world-running.rst index 8e604f8d11..b71eefd867 100644 --- a/docs/source/hello-world-running.rst +++ b/docs/source/hello-world-running.rst @@ -86,16 +86,12 @@ If we navigate to one of these folders, we'll see four node folder. Each node fo .. code:: python . - // The runnable node - |____corda.jar - // The node's webserver - |____corda-webserver.jar + |____corda.jar // The runnable node + |____corda-webserver.jar // The node's webserver |____dependencies - // The node's configuration file - |____node.conf + |____node.conf // The node's configuration file |____plugins - // Our IOU CorDapp - |____java/kotlin-source-0.1.jar + |____java/kotlin-source-0.1.jar // Our IOU CorDapp Let's start the nodes by running the following commands from the root of the project: @@ -132,9 +128,15 @@ commands. We want to create an IOU of 100 with Node B. We start the ``IOUFlow`` by typing: -.. code:: python +.. container:: codeset - start IOUFlow arg0: 99, arg1: "CN=NodeB,O=NodeB,L=New York,C=US" + .. code-block:: java + + start IOUFlow arg0: 99, arg1: "NodeB" + + .. code-block:: kotlin + + start IOUFlow iouValue: 99, otherParty: "NodeB" Node A and Node B will automatically agree an IOU. @@ -162,8 +164,8 @@ The vaults of Node A and Node B should both display the following output: - state: data: value: 99 - sender: "CN=NodeA,O=NodeA,L=London,C=GB" - recipient: "CN=NodeB,O=NodeB,L=New York,C=US" + lender: "CN=NodeA,O=NodeA,L=London,C=GB" + borrower: "CN=NodeB,O=NodeB,L=New York,C=US" contract: legalContractReference: "559322B95BCF7913E3113962DC3F3CBD71C818C66977721580C045DC41C813A5" participants: @@ -190,19 +192,26 @@ CorDapp is made up of three key parts: * The ``IOUState``, representing IOUs on the ledger * The ``IOUContract``, controlling the evolution of IOUs over time -* The ``IOUFlow``, orchestrating the process of agreeing the creation of an IOU on-ledger. +* The ``IOUFlow``, orchestrating the process of agreeing the creation of an IOU on-ledger Together, these three parts completely determine how IOUs are created and evolved on the ledger. Next steps ---------- -You should now be ready to develop your own CorDapps. There's -`a more fleshed-out version of the IOU CorDapp `_ -with an API and web front-end, and a set of example CorDapps in -`the main Corda repo `_, under ``samples``. An explanation of how to run these -samples :doc:`here `. +There are a number of improvements we could make to this CorDapp: + +* We could require signatures from the lender as well the borrower, to give both parties a say in the creation of a new + ``IOUState`` +* We should add unit tests, using the contract-test and flow-test frameworks +* We should change ``IOUState.value`` from an integer to a proper amount of a given currency +* We could add an API, to make it easier to interact with the CorDapp + +We will explore some of these improvements in future tutorials. But you should now be ready to develop your own +CorDapps. There's `a more fleshed-out version of the IOU CorDapp `_ with an +API and web front-end, and a set of example CorDapps in `the main Corda repo `_, under +``samples``. An explanation of how to run these samples :doc:`here `. As you write CorDapps, you can learn more about the API available :doc:`here `. If you get stuck at any point, please reach out on `Slack `_, -`Discourse `_, or `Stack Overflow `_. \ No newline at end of file +`Discourse `_, or `Stack Overflow `_. diff --git a/docs/source/hello-world-state.rst b/docs/source/hello-world-state.rst index 7196bcbd7a..b677366a08 100644 --- a/docs/source/hello-world-state.rst +++ b/docs/source/hello-world-state.rst @@ -38,11 +38,6 @@ If you do want to dive into Kotlin, there's an official `getting started guide `_, and a series of `Kotlin Koans `_. -If not, here's a quick primer on the Kotlinisms in the declaration of ``ContractState``: - -* ``val`` declares a read-only property, similar to Java's ``final`` keyword -* The syntax ``varName: varType`` declares ``varName`` as being of type ``varType`` - We can see that the ``ContractState`` interface declares two properties: * ``contract``: the contract controlling transactions involving this state @@ -53,15 +48,15 @@ Beyond this, our state is free to define any properties, methods, helpers or inn represent a given class of shared facts on the ledger. ``ContractState`` also has several child interfaces that you may wish to implement depending on your state, such as -``LinearState`` and ``OwnableState``. +``LinearState`` and ``OwnableState``. See :doc:`api-states` for more information. Modelling IOUs -------------- How should we define the ``IOUState`` representing IOUs on the ledger? Beyond implementing the ``ContractState`` interface, our ``IOUState`` will also need properties to track the relevant features of the IOU: -* The sender of the IOU -* The IOU's recipient +* The lender of the IOU +* The borrower of the IOU * The value of the IOU There are many more fields you could include, such as the IOU's currency. We'll abstract them away for now. If @@ -76,23 +71,22 @@ define an ``IOUState``: .. code-block:: kotlin - package com.template + package com.iou import net.corda.core.contracts.ContractState import net.corda.core.identity.Party class IOUState(val value: Int, - val sender: Party, - val recipient: Party, - // TODO: Once we've defined IOUContract, come back and update this. - override val contract: TemplateContract = TemplateContract()) : ContractState { + val lender: Party, + val borrower: Party) : ContractState { + override val contract: IOUContract = IOUContract() - override val participants get() = listOf(sender, recipient) + override val participants get() = listOf(lender, borrower) } .. code-block:: java - package com.template; + package com.iou; import com.google.common.collect.ImmutableList; import net.corda.core.contracts.ContractState; @@ -103,53 +97,52 @@ define an ``IOUState``: public class IOUState implements ContractState { private final int value; - private final Party sender; - private final Party recipient; - // TODO: Once we've defined IOUContract, come back and update this. - private final TemplateContract contract; + private final Party lender; + private final Party borrower; + private final IOUContract contract = new IOUContract(); - public IOUState(int value, Party sender, Party recipient, IOUContract contract) { + public IOUState(int value, Party lender, Party borrower) { this.value = value; - this.sender = sender; - this.recipient = recipient; - this.contract = contract; + this.lender = lender; + this.borrower = borrower; } public int getValue() { return value; } - public Party getSender() { - return sender; + public Party getLender() { + return lender; } - public Party getRecipient() { - return recipient; + public Party getBorrower() { + return borrower; } @Override // TODO: Once we've defined IOUContract, come back and update this. - public TemplateContract getContract() { + public IOUContract getContract() { return contract; } @Override public List getParticipants() { - return ImmutableList.of(sender, recipient); + return ImmutableList.of(lender, borrower); } } We've made the following changes: * We've renamed ``TemplateState`` to ``IOUState`` -* We've added properties for ``value``, ``sender`` and ``recipient`` (along with any getters and setters in Java): +* We've added properties for ``value``, ``lender`` and ``borrower`` (along with any getters and setters in Java): - * ``value`` is just a standard int (in Java)/Int (in Kotlin), but ``sender`` and ``recipient`` are of type - ``Party``. ``Party`` is a built-in Corda type that represents an entity on the network. + * ``value`` is just a standard int (in Java)/Int (in Kotlin) + * ``lender`` and ``borrower`` are of type ``Party``. ``Party`` is a built-in Corda type that represents an entity on + the network. -* We've overridden ``participants`` to return a list of the ``sender`` and ``recipient`` -* This means that actions such as changing the state's contract or its notary will require approval from both the - ``sender`` and the ``recipient`` +* We've overridden ``participants`` to return a list of the ``lender`` and ``borrower`` + + * Actions such as changing a state's contract or notary will require approval from all the ``participants`` We've left ``IOUState``'s contract as ``TemplateContract`` for now. We'll update this once we've defined the ``IOUContract``. diff --git a/docs/source/hello-world-template.rst b/docs/source/hello-world-template.rst index d38350571f..aac84d483d 100644 --- a/docs/source/hello-world-template.rst +++ b/docs/source/hello-world-template.rst @@ -38,7 +38,7 @@ We can write our CorDapp in either Java or Kotlin, and will be providing the cod you want to write the CorDapp in Java, you'll be modifying the files under ``java-source``. If you prefer to use Kotlin, you'll be modifying the files under ``kotlin-source``. -To implement our IOU CorDapp, we'll only need to modify five files: +To implement our IOU CorDapp, we'll only need to modify three files: .. container:: codeset @@ -53,13 +53,6 @@ To implement our IOU CorDapp, we'll only need to modify five files: // 3. The flow java-source/src/main/java/com/template/flow/TemplateFlow.java - // Tests for our contract and flow: - // 1. The contract tests - java-source/src/test/java/com/template/contract/ContractTests.java - - // 2. The flow tests - java-source/src/test/java/com/template/flow/FlowTests.java - .. code-block:: kotlin // 1. The state @@ -71,13 +64,6 @@ To implement our IOU CorDapp, we'll only need to modify five files: // 3. The flow kotlin-source/src/main/kotlin/com/template/flow/TemplateFlow.kt - // Tests for our contract and flow: - // 1. The contract tests - kotlin-source/src/test/kotlin/com/template/contract/ContractTests.kt - - // 2. The flow tests - kotlin-source/src/test/kotlin/com/template/flow/FlowTests.kt - Progress so far --------------- We now have a template that we can build upon to define our IOU CorDapp. diff --git a/docs/source/resources/simple-tutorial-flow.png b/docs/source/resources/simple-tutorial-flow.png new file mode 100644 index 0000000000000000000000000000000000000000..7faf9a24629150cbbda1c7a115d9cdec5e535ffa GIT binary patch literal 153908 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 AO#lD@ literal 0 HcmV?d00001 diff --git a/docs/source/resources/simple-tutorial-transaction.png b/docs/source/resources/simple-tutorial-transaction.png new file mode 100644 index 0000000000000000000000000000000000000000..f881346b06a881de39419087c270dc89900a3a0b GIT binary patch literal 176926 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�VUiPO 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 lVUTT1m_nnQh8my>vhXuqlbfIVw; Date: Thu, 6 Jul 2017 17:27:44 +0100 Subject: [PATCH 73/97] Moved random63BitValue() to CryptoUtils --- .../corda/client/rpc/CordaRPCClientTest.kt | 2 +- .../net/corda/client/rpc/RPCStabilityTests.kt | 1 + .../corda/client/rpc/internal/RPCClient.kt | 2 +- .../rpc/internal/RPCClientProxyHandler.kt | 2 +- .../corda/client/rpc/RPCConcurrencyTests.kt | 2 +- core/src/main/kotlin/net/corda/core/Utils.kt | 7 ------- .../kotlin/net/corda/core/crypto/Crypto.kt | 2 +- .../net/corda/core/crypto/CryptoUtils.kt | 20 ++++++++++++++++--- docs/source/changelog.rst | 2 ++ .../net/corda/contracts/CommercialPaper.kt | 2 +- .../net/corda/contracts/asset/Obligation.kt | 2 +- .../services/messaging/MQSecurityTest.kt | 2 +- .../services/messaging/P2PMessagingTest.kt | 1 + .../services/messaging/P2PSecurityTest.kt | 2 +- .../services/messaging/NodeMessagingClient.kt | 1 + .../node/services/messaging/RPCServer.kt | 2 +- .../services/network/NetworkMapService.kt | 2 +- .../statemachine/FlowStateMachineImpl.kt | 2 +- .../statemachine/StateMachineManager.kt | 1 + .../OutOfProcessTransactionVerifierService.kt | 2 +- .../statemachine/FlowFrameworkTests.kt | 1 + .../kotlin/net/corda/testing/RPCDriver.kt | 2 +- .../kotlin/net/corda/testing/node/MockNode.kt | 1 + .../net/corda/verifier/VerifierDriver.kt | 2 +- 24 files changed, 40 insertions(+), 25 deletions(-) diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt index f7062286ab..40a179fd29 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt @@ -5,7 +5,7 @@ import net.corda.core.flows.FlowInitiator import net.corda.core.getOrThrow import net.corda.core.messaging.* import net.corda.core.node.services.ServiceInfo -import net.corda.core.random63BitValue +import net.corda.core.crypto.random63BitValue import net.corda.core.utilities.OpaqueBytes import net.corda.testing.ALICE import net.corda.flows.CashException diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt index 4cf43156d2..4a9dad7a36 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt @@ -10,6 +10,7 @@ import com.google.common.util.concurrent.Futures import net.corda.client.rpc.internal.RPCClient import net.corda.client.rpc.internal.RPCClientConfiguration import net.corda.core.* +import net.corda.core.crypto.random63BitValue import net.corda.core.messaging.RPCOps import net.corda.testing.driver.poll import net.corda.node.services.messaging.RPCServerConfiguration diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt index 3e52dbd946..0412aeac36 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt @@ -4,7 +4,7 @@ import com.google.common.net.HostAndPort import net.corda.core.logElapsedTime import net.corda.core.messaging.RPCOps import net.corda.core.minutes -import net.corda.core.random63BitValue +import net.corda.core.crypto.random63BitValue import net.corda.core.seconds import net.corda.core.utilities.loggerFor import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt index 86dcf7a7d1..9d38896c19 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt @@ -14,7 +14,7 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder import net.corda.core.ThreadBox import net.corda.core.getOrThrow import net.corda.core.messaging.RPCOps -import net.corda.core.random63BitValue +import net.corda.core.crypto.random63BitValue import net.corda.core.serialization.KryoPoolWithContext import net.corda.core.utilities.* import net.corda.nodeapi.* diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCConcurrencyTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCConcurrencyTests.kt index 2ffe065832..fb283773d1 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCConcurrencyTests.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCConcurrencyTests.kt @@ -4,7 +4,7 @@ import net.corda.client.rpc.internal.RPCClientConfiguration import net.corda.core.future import net.corda.core.messaging.RPCOps import net.corda.core.millis -import net.corda.core.random63BitValue +import net.corda.core.crypto.random63BitValue import net.corda.core.serialization.CordaSerializable import net.corda.node.services.messaging.RPCServerConfiguration import net.corda.testing.RPCDriverExposedDSLInterface diff --git a/core/src/main/kotlin/net/corda/core/Utils.kt b/core/src/main/kotlin/net/corda/core/Utils.kt index 021e6e194d..541240ae11 100644 --- a/core/src/main/kotlin/net/corda/core/Utils.kt +++ b/core/src/main/kotlin/net/corda/core/Utils.kt @@ -7,7 +7,6 @@ import com.google.common.base.Throwables import com.google.common.io.ByteStreams import com.google.common.util.concurrent.* import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.newSecureRandom import net.corda.core.crypto.sha256 import net.corda.core.flows.FlowException import net.corda.core.serialization.CordaSerializable @@ -59,12 +58,6 @@ infix fun Int.checkedAdd(b: Int) = Math.addExact(this, b) @Suppress("unused") infix fun Long.checkedAdd(b: Long) = Math.addExact(this, b) -/** - * Returns a random positive long generated using a secure RNG. This function sacrifies a bit of entropy in order to - * avoid potential bugs where the value is used in a context where negative numbers are not expected. - */ -fun random63BitValue(): Long = Math.abs(newSecureRandom().nextLong()) - /** Same as [Future.get] but with a more descriptive name, and doesn't throw [ExecutionException], instead throwing its cause */ fun Future.getOrThrow(timeout: Duration? = null): T { return try { diff --git a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt index b795c24ad6..a92ab97845 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt @@ -1,6 +1,6 @@ package net.corda.core.crypto -import net.corda.core.random63BitValue +import net.corda.core.crypto.random63BitValue import net.i2p.crypto.eddsa.EdDSAEngine import net.i2p.crypto.eddsa.EdDSAPrivateKey import net.i2p.crypto.eddsa.EdDSAPublicKey diff --git a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt index 0f0b73ea0a..310f34ed7f 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt @@ -201,9 +201,23 @@ fun secureRandomBytes(numOfBytes: Int): ByteArray { */ @Throws(NoSuchAlgorithmException::class) fun newSecureRandom(): SecureRandom { - if (System.getProperty("os.name") == "Linux") { - return SecureRandom.getInstance("NativePRNGNonBlocking") + return if (System.getProperty("os.name") == "Linux") { + SecureRandom.getInstance("NativePRNGNonBlocking") } else { - return SecureRandom.getInstanceStrong() + SecureRandom.getInstanceStrong() + } +} + +/** + * Returns a random positive non-zero long generated using a secure RNG. This function sacrifies a bit of entropy in order + * to avoid potential bugs where the value is used in a context where negative numbers or zero are not expected. + */ +fun random63BitValue(): Long { + while (true) { + val candidate = Math.abs(newSecureRandom().nextLong()) + // No need to check for -0L + if (candidate != 0L && candidate != Long.MIN_VALUE) { + return candidate + } } } diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index b0a46f0cab..85c1d923a5 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -23,6 +23,8 @@ UNRELEASED * In Java, ``QueryCriteriaUtilsKt`` has moved to ``QueryCriteriaUtils``. Also ``and`` and ``or`` are now instance methods of ``QueryCrtieria``. +* ``random63BitValue()`` has moved to ``CryptoUtils`` + Milestone 13 ------------ diff --git a/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt b/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt index 0bfeb4ea50..f3d2fcdb2e 100644 --- a/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt +++ b/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt @@ -13,7 +13,7 @@ import net.corda.core.crypto.toBase58String import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.node.services.VaultService -import net.corda.core.random63BitValue +import net.corda.core.crypto.random63BitValue import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState import net.corda.core.schemas.QueryableState diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt b/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt index 02bff607b4..dbc7cf54dc 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt @@ -14,7 +14,7 @@ import net.corda.core.crypto.testing.NULL_PARTY import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party -import net.corda.core.random63BitValue +import net.corda.core.crypto.random63BitValue import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.Emoji diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt index f549064a6f..3b99f6845f 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt @@ -11,7 +11,7 @@ import net.corda.core.flows.InitiatingFlow import net.corda.core.getOrThrow import net.corda.core.identity.Party import net.corda.core.messaging.CordaRPCOps -import net.corda.core.random63BitValue +import net.corda.core.crypto.random63BitValue import net.corda.testing.ALICE import net.corda.testing.BOB import net.corda.core.utilities.unwrap diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt index af7c8635bd..d9eb27dbc9 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt @@ -3,6 +3,7 @@ package net.corda.services.messaging import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import net.corda.core.* +import net.corda.core.crypto.random63BitValue import net.corda.core.messaging.MessageRecipients import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.services.DEFAULT_SESSION_ID diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt index 3df678b855..da327f6417 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt @@ -6,7 +6,7 @@ import net.corda.core.crypto.X509Utilities import net.corda.core.crypto.cert import net.corda.core.getOrThrow import net.corda.core.node.NodeInfo -import net.corda.core.random63BitValue +import net.corda.core.crypto.random63BitValue import net.corda.core.seconds import net.corda.node.internal.NetworkMapInfo import net.corda.node.services.config.configureWithDevSSLCertificate diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt index 0f73b693ea..beaa4ddc12 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt @@ -3,6 +3,7 @@ package net.corda.node.services.messaging import com.google.common.net.HostAndPort import com.google.common.util.concurrent.ListenableFuture import net.corda.core.* +import net.corda.core.crypto.random63BitValue import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.MessageRecipients import net.corda.core.messaging.RPCOps diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt index 1306c4206b..05c13f6341 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt @@ -14,7 +14,7 @@ import com.google.common.collect.SetMultimap import com.google.common.util.concurrent.ThreadFactoryBuilder import net.corda.core.ErrorOr import net.corda.core.messaging.RPCOps -import net.corda.core.random63BitValue +import net.corda.core.crypto.random63BitValue import net.corda.core.seconds import net.corda.core.serialization.KryoPoolWithContext import net.corda.core.utilities.LazyStickyPool diff --git a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt index ce8319ebcb..d0f51b534e 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt @@ -13,7 +13,7 @@ import net.corda.core.node.services.DEFAULT_SESSION_ID import net.corda.core.node.services.KeyManagementService import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.ServiceType -import net.corda.core.random63BitValue +import net.corda.core.crypto.random63BitValue import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.deserialize diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt index 0b7d085b18..537597f833 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt @@ -13,7 +13,7 @@ import net.corda.core.crypto.SecureHash import net.corda.core.flows.* import net.corda.core.identity.Party import net.corda.core.internal.FlowStateMachine -import net.corda.core.random63BitValue +import net.corda.core.crypto.random63BitValue import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.UntrustworthyData diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt index 3e1a8bdf35..fcae71e21b 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt @@ -17,6 +17,7 @@ import com.google.common.util.concurrent.MoreExecutors import io.requery.util.CloseableIterator import net.corda.core.* import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.random63BitValue import net.corda.core.flows.FlowException import net.corda.core.flows.FlowInitiator import net.corda.core.flows.FlowLogic diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/OutOfProcessTransactionVerifierService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/OutOfProcessTransactionVerifierService.kt index e0d2999526..1539455fd9 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/OutOfProcessTransactionVerifierService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/OutOfProcessTransactionVerifierService.kt @@ -6,7 +6,7 @@ import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.SettableFuture import net.corda.core.crypto.SecureHash import net.corda.core.node.services.TransactionVerifierService -import net.corda.core.random63BitValue +import net.corda.core.crypto.random63BitValue import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.LedgerTransaction import net.corda.core.utilities.loggerFor diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt index e59e703dec..26ea5b06f3 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt @@ -11,6 +11,7 @@ import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.testing.DummyState import net.corda.core.crypto.SecureHash import net.corda.core.crypto.generateKeyPair +import net.corda.core.crypto.random63BitValue import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowSessionException diff --git a/test-utils/src/main/kotlin/net/corda/testing/RPCDriver.kt b/test-utils/src/main/kotlin/net/corda/testing/RPCDriver.kt index f4d09c8d23..2276f19301 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/RPCDriver.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/RPCDriver.kt @@ -11,7 +11,7 @@ import net.corda.client.rpc.internal.RPCClientConfiguration import net.corda.core.div import net.corda.core.map import net.corda.core.messaging.RPCOps -import net.corda.core.random63BitValue +import net.corda.core.crypto.random63BitValue import net.corda.testing.driver.ProcessUtilities import net.corda.node.services.RPCUserService import net.corda.node.services.messaging.ArtemisMessagingServer diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt index 05f3c115be..0dfd96d2fe 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -10,6 +10,7 @@ import net.corda.core.* import net.corda.core.crypto.CertificateAndKeyPair import net.corda.core.crypto.cert import net.corda.core.crypto.entropyToKeyPair +import net.corda.core.crypto.random63BitValue import net.corda.core.identity.PartyAndCertificate import net.corda.core.messaging.MessageRecipients import net.corda.core.messaging.RPCOps diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt index 13cdb8d6aa..f8adb129b1 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt @@ -11,7 +11,7 @@ import net.corda.core.crypto.X509Utilities import net.corda.core.crypto.commonName import net.corda.core.div import net.corda.core.map -import net.corda.core.random63BitValue +import net.corda.core.crypto.random63BitValue import net.corda.core.transactions.LedgerTransaction import net.corda.testing.driver.ProcessUtilities import net.corda.core.utilities.loggerFor From cefa14507abc234bba1064617144fa78a4d1562c Mon Sep 17 00:00:00 2001 From: Andrzej Cichocki Date: Fri, 7 Jul 2017 15:11:07 +0100 Subject: [PATCH 74/97] Retire HostAndPort (#962) * Don't attempt to parse a resolved InetSocketAddress toString * A mock node isn't reachable via an address --- .../client/jfx/model/NodeMonitorModel.kt | 4 +- .../net/corda/client/rpc/RPCStabilityTests.kt | 4 +- .../net/corda/client/rpc/CordaRPCClient.kt | 4 +- .../corda/client/rpc/internal/RPCClient.kt | 4 +- .../kotlin/net/corda/core/node/NodeInfo.kt | 4 +- .../core/utilities/NetworkHostAndPort.kt | 37 +++++++++++ .../CollectionExtensionTests.kt | 4 +- .../core/utilities/NetworkHostAndPortTest.kt | 60 ++++++++++++++++++ docs/source/changelog.rst | 2 +- .../nodeapi/ArtemisMessagingComponent.kt | 13 ++-- .../net/corda/nodeapi/ArtemisTcpTransport.kt | 4 +- .../corda/nodeapi/config/ConfigUtilities.kt | 7 ++- .../nodeapi/serialization/DefaultWhitelist.kt | 4 +- .../corda/nodeapi/config/ConfigParsingTest.kt | 14 ++--- .../node/services/BFTNotaryServiceTests.kt | 4 +- .../services/messaging/MQSecurityTest.kt | 6 +- .../net/corda/node/internal/AbstractNode.kt | 4 +- .../kotlin/net/corda/node/internal/Node.kt | 21 ++++--- .../node/services/config/NodeConfiguration.kt | 16 ++--- .../messaging/ArtemisMessagingServer.kt | 8 +-- .../services/messaging/NodeMessagingClient.kt | 6 +- .../services/transactions/BFTSMaRtConfig.kt | 12 ++-- .../transactions/RaftUniquenessProvider.kt | 5 +- .../net/corda/node/utilities/ConfigUtils.kt | 9 --- .../config/FullNodeConfigurationTest.kt | 6 +- .../messaging/ArtemisMessagingTests.kt | 6 +- .../transactions/BFTSMaRtConfigTests.kt | 4 +- .../DistributedImmutableMapTests.kt | 4 +- .../corda/attachmentdemo/AttachmentDemo.kt | 5 +- .../net/corda/bank/BankOfCordaDriver.kt | 6 +- .../corda/bank/api/BankOfCordaClientApi.kt | 4 +- .../kotlin/net/corda/irs/IRSDemoTest.kt | 4 +- .../src/test/kotlin/net/corda/irs/IRSDemo.kt | 8 +-- .../kotlin/net/corda/irs/IrsDemoClientApi.kt | 4 +- .../net/corda/demorun/util/DemoUtils.kt | 4 +- .../net/corda/notarydemo/BFTNotaryCordform.kt | 4 +- .../kotlin/net/corda/notarydemo/Notarise.kt | 4 +- .../corda/notarydemo/RaftNotaryCordform.kt | 4 +- .../kotlin/net/corda/traderdemo/TraderDemo.kt | 6 +- .../net/corda/smoketesting/NodeProcess.kt | 4 +- .../kotlin/net/corda/testing/CoreTestUtils.kt | 10 +-- .../main/kotlin/net/corda/testing/NodeApi.kt | 62 ------------------- .../kotlin/net/corda/testing/RPCDriver.kt | 35 ++++++----- .../kotlin/net/corda/testing/driver/Driver.kt | 31 +++++----- .../testing/driver/NetworkMapStartStrategy.kt | 4 +- .../kotlin/net/corda/testing/http/HttpApi.kt | 4 +- .../corda/testing/messaging/SimpleMQClient.kt | 4 +- .../corda/testing/node/MockNetworkMapCache.kt | 6 +- .../kotlin/net/corda/testing/node/MockNode.kt | 4 +- .../net/corda/testing/node/MockServices.kt | 3 +- .../net/corda/testing/node/SimpleNode.kt | 6 +- .../corda/demobench/model/InstallFactory.kt | 4 +- .../kotlin/net/corda/demobench/rpc/NodeRPC.kt | 4 +- .../corda/demobench/model/NodeConfigTest.kt | 4 +- .../net/corda/explorer/views/LoginView.kt | 6 +- .../net/corda/loadtest/ConnectionManager.kt | 4 +- .../net/corda/loadtest/NodeConnection.kt | 4 +- .../net/corda/verifier/VerifierDriver.kt | 14 ++--- .../kotlin/net/corda/verifier/Verifier.kt | 4 +- .../corda/webserver/WebserverDriverTests.kt | 4 +- .../net/corda/webserver/WebServerConfig.kt | 6 +- 61 files changed, 290 insertions(+), 266 deletions(-) create mode 100644 core/src/main/kotlin/net/corda/core/utilities/NetworkHostAndPort.kt rename core/src/test/kotlin/net/corda/core/{utilities => }/CollectionExtensionTests.kt (92%) create mode 100644 core/src/test/kotlin/net/corda/core/utilities/NetworkHostAndPortTest.kt delete mode 100644 node/src/main/kotlin/net/corda/node/utilities/ConfigUtils.kt delete mode 100644 test-utils/src/main/kotlin/net/corda/testing/NodeApi.kt diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt index 5a5e79cc34..e2b134bc8c 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt @@ -1,6 +1,5 @@ package net.corda.client.jfx.model -import com.google.common.net.HostAndPort import javafx.beans.property.SimpleObjectProperty import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.CordaRPCClientConfiguration @@ -13,6 +12,7 @@ import net.corda.core.node.services.NetworkMapCache.MapChange import net.corda.core.node.services.Vault import net.corda.core.seconds import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.NetworkHostAndPort import rx.Observable import rx.subjects.PublishSubject @@ -51,7 +51,7 @@ class NodeMonitorModel { * Register for updates to/from a given vault. * TODO provide an unsubscribe mechanism */ - fun register(nodeHostAndPort: HostAndPort, username: String, password: String) { + fun register(nodeHostAndPort: NetworkHostAndPort, username: String, password: String) { val client = CordaRPCClient( hostAndPort = nodeHostAndPort, configuration = CordaRPCClientConfiguration.default.copy( diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt index 4a9dad7a36..a76ae661ad 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt @@ -5,13 +5,13 @@ import com.esotericsoftware.kryo.Serializer import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output import com.esotericsoftware.kryo.pool.KryoPool -import com.google.common.net.HostAndPort import com.google.common.util.concurrent.Futures import net.corda.client.rpc.internal.RPCClient import net.corda.client.rpc.internal.RPCClientConfiguration import net.corda.core.* import net.corda.core.crypto.random63BitValue import net.corda.core.messaging.RPCOps +import net.corda.core.utilities.NetworkHostAndPort import net.corda.testing.driver.poll import net.corda.node.services.messaging.RPCServerConfiguration import net.corda.nodeapi.RPCApi @@ -78,7 +78,7 @@ class RPCStabilityTests { val executor = Executors.newScheduledThreadPool(1) fun startAndStop() { rpcDriver { - ErrorOr.catch { startRpcClient(HostAndPort.fromString("localhost:9999")).get() } + ErrorOr.catch { startRpcClient(NetworkHostAndPort("localhost", 9999)).get() } val server = startRpcServer(ops = DummyOps) ErrorOr.catch { startRpcClient( server.get().broker.hostAndPort!!, diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt index 199cdd6d67..78baa8d906 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt @@ -1,9 +1,9 @@ package net.corda.client.rpc -import com.google.common.net.HostAndPort import net.corda.client.rpc.internal.RPCClient import net.corda.client.rpc.internal.RPCClientConfiguration import net.corda.core.messaging.CordaRPCOps +import net.corda.core.utilities.NetworkHostAndPort import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport import net.corda.nodeapi.ConnectionDirection import net.corda.nodeapi.config.SSLConfiguration @@ -33,7 +33,7 @@ data class CordaRPCClientConfiguration( /** @see RPCClient */ class CordaRPCClient( - hostAndPort: HostAndPort, + hostAndPort: NetworkHostAndPort, sslConfiguration: SSLConfiguration? = null, configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default ) { diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt index 0412aeac36..a792c24faa 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt @@ -1,11 +1,11 @@ package net.corda.client.rpc.internal -import com.google.common.net.HostAndPort import net.corda.core.logElapsedTime import net.corda.core.messaging.RPCOps import net.corda.core.minutes import net.corda.core.crypto.random63BitValue import net.corda.core.seconds +import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.loggerFor import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport import net.corda.nodeapi.ConnectionDirection @@ -88,7 +88,7 @@ class RPCClient( val rpcConfiguration: RPCClientConfiguration = RPCClientConfiguration.default ) { constructor( - hostAndPort: HostAndPort, + hostAndPort: NetworkHostAndPort, sslConfiguration: SSLConfiguration? = null, configuration: RPCClientConfiguration = RPCClientConfiguration.default ) : this(tcpTransport(ConnectionDirection.Outbound(), hostAndPort, sslConfiguration), configuration) diff --git a/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt b/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt index fc593fdfce..342e9997e1 100644 --- a/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt +++ b/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt @@ -1,11 +1,11 @@ package net.corda.core.node -import com.google.common.net.HostAndPort import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceType import net.corda.core.serialization.CordaSerializable +import net.corda.core.utilities.NetworkHostAndPort /** * Information for an advertised service including the service specific identity information. @@ -19,7 +19,7 @@ data class ServiceEntry(val info: ServiceInfo, val identity: PartyAndCertificate */ // TODO We currently don't support multi-IP/multi-identity nodes, we only left slots in the data structures. @CordaSerializable -data class NodeInfo(val addresses: List, +data class NodeInfo(val addresses: List, val legalIdentityAndCert: PartyAndCertificate, //TODO This field will be removed in future PR which gets rid of services. val legalIdentitiesAndCerts: Set, val platformVersion: Int, diff --git a/core/src/main/kotlin/net/corda/core/utilities/NetworkHostAndPort.kt b/core/src/main/kotlin/net/corda/core/utilities/NetworkHostAndPort.kt new file mode 100644 index 0000000000..28d0e5c7dc --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/utilities/NetworkHostAndPort.kt @@ -0,0 +1,37 @@ +package net.corda.core.utilities + +import java.net.URI + +/** + * Tuple of host and port. Use [parseNetworkHostAndPort] on untrusted data. + * @param host a hostname or IP address. IPv6 addresses must not be enclosed in square brackets. + * @param port a valid port number. + */ +data class NetworkHostAndPort(val host: String, val port: Int) { + companion object { + internal val invalidPortFormat = "Invalid port: %s" + internal val unparseableAddressFormat = "Unparseable address: %s" + internal val missingPortFormat = "Missing port: %s" + } + + init { + require(port in (0..0xffff)) { invalidPortFormat.format(port) } + } + + override fun toString() = if (':' in host) "[$host]:$port" else "$host:$port" +} + +/** + * Parses a string of the form host:port into a [NetworkHostAndPort]. + * The host part may be a hostname or IP address. If it's an IPv6 address, it must be enclosed in square brackets. + * Note this does not parse the toString of a resolved [java.net.InetSocketAddress], which is of a host/IP:port form. + * @throws IllegalArgumentException if the port is missing, the string is garbage, or the NetworkHostAndPort constructor rejected the parsed parts. + */ +fun String.parseNetworkHostAndPort() = run { + val uri = URI(null, this, null, null, null) + require(uri.host != null) { NetworkHostAndPort.unparseableAddressFormat.format(this) } + require(uri.port != -1) { NetworkHostAndPort.missingPortFormat.format(this) } + NetworkHostAndPort(bracketedHost.matchEntire(uri.host)?.groupValues?.get(1) ?: uri.host, uri.port) +} + +private val bracketedHost = "\\[(.*)]".toRegex() diff --git a/core/src/test/kotlin/net/corda/core/utilities/CollectionExtensionTests.kt b/core/src/test/kotlin/net/corda/core/CollectionExtensionTests.kt similarity index 92% rename from core/src/test/kotlin/net/corda/core/utilities/CollectionExtensionTests.kt rename to core/src/test/kotlin/net/corda/core/CollectionExtensionTests.kt index 169fab9793..1fab9fceaa 100644 --- a/core/src/test/kotlin/net/corda/core/utilities/CollectionExtensionTests.kt +++ b/core/src/test/kotlin/net/corda/core/CollectionExtensionTests.kt @@ -1,7 +1,5 @@ -package net.corda.core.utilities +package net.corda.core -import net.corda.core.indexOfOrThrow -import net.corda.core.noneOrSingle import org.junit.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith diff --git a/core/src/test/kotlin/net/corda/core/utilities/NetworkHostAndPortTest.kt b/core/src/test/kotlin/net/corda/core/utilities/NetworkHostAndPortTest.kt new file mode 100644 index 0000000000..925773e2b3 --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/utilities/NetworkHostAndPortTest.kt @@ -0,0 +1,60 @@ +package net.corda.core.utilities + +import org.junit.Test +import kotlin.test.assertEquals +import org.assertj.core.api.Assertions.assertThatThrownBy + +class NetworkHostAndPortTest { + /** + * If a host isn't known-good it should go via the parser, which does some validation. + */ + @Test + fun `constructor is not fussy about host`() { + assertEquals("", NetworkHostAndPort("", 1234).host) + assertEquals("x", NetworkHostAndPort("x", 1234).host) + assertEquals("500", NetworkHostAndPort("500", 1234).host) + assertEquals(" yo yo\t", NetworkHostAndPort(" yo yo\t", 1234).host) + assertEquals("[::1]", NetworkHostAndPort("[::1]", 1234).host) // Don't do this. + } + + @Test + fun `constructor requires a valid port`() { + assertEquals(0, NetworkHostAndPort("example.com", 0).port) + assertEquals(65535, NetworkHostAndPort("example.com", 65535).port) + listOf(65536, -1).forEach { + assertThatThrownBy { + NetworkHostAndPort("example.com", it) + }.isInstanceOf(IllegalArgumentException::class.java).hasMessage(NetworkHostAndPort.invalidPortFormat.format(it)) + } + } + + @Test + fun `toString works`() { + assertEquals("example.com:1234", NetworkHostAndPort("example.com", 1234).toString()) + assertEquals("example.com:65535", NetworkHostAndPort("example.com", 65535).toString()) + assertEquals("1.2.3.4:1234", NetworkHostAndPort("1.2.3.4", 1234).toString()) + assertEquals("[::1]:1234", NetworkHostAndPort("::1", 1234).toString()) + // Brackets perhaps not necessary in unabbreviated case, but URI seems to need them for parsing: + assertEquals("[0:0:0:0:0:0:0:1]:1234", NetworkHostAndPort("0:0:0:0:0:0:0:1", 1234).toString()) + assertEquals(":1234", NetworkHostAndPort("", 1234).toString()) // URI won't parse this. + } + + @Test + fun `parseNetworkHostAndPort works`() { + assertEquals(NetworkHostAndPort("example.com", 1234), "example.com:1234".parseNetworkHostAndPort()) + assertEquals(NetworkHostAndPort("example.com", 65535), "example.com:65535".parseNetworkHostAndPort()) + assertEquals(NetworkHostAndPort("1.2.3.4", 1234), "1.2.3.4:1234".parseNetworkHostAndPort()) + assertEquals(NetworkHostAndPort("::1", 1234), "[::1]:1234".parseNetworkHostAndPort()) + assertEquals(NetworkHostAndPort("0:0:0:0:0:0:0:1", 1234), "[0:0:0:0:0:0:0:1]:1234".parseNetworkHostAndPort()) + listOf("0:0:0:0:0:0:0:1:1234", ":1234", "example.com:-1").forEach { + assertThatThrownBy { + it.parseNetworkHostAndPort() + }.isInstanceOf(IllegalArgumentException::class.java).hasMessage(NetworkHostAndPort.unparseableAddressFormat.format(it)) + } + listOf("example.com:", "example.com").forEach { + assertThatThrownBy { + it.parseNetworkHostAndPort() + }.isInstanceOf(IllegalArgumentException::class.java).hasMessage(NetworkHostAndPort.missingPortFormat.format(it)) + } + } +} diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 85c1d923a5..c2ab1cfba8 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -12,7 +12,7 @@ UNRELEASED * ``PhysicalLocation`` was renamed to ``WorldMapLocation`` to emphasise that it doesn't need to map to a truly physical location of the node server. * Slots for multiple IP addresses and ``legalIdentitiesAndCert``s were introduced. Addresses are no longer of type - ``SingleMessageRecipient``, but of ``HostAndPort``. + ``SingleMessageRecipient``, but of ``NetworkHostAndPort``. * ``ServiceHub.storageService`` has been removed. ``attachments`` and ``validatedTransactions`` are now direct members of ``ServiceHub``. diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisMessagingComponent.kt b/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisMessagingComponent.kt index f31b75eaaa..a37a841f83 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisMessagingComponent.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisMessagingComponent.kt @@ -1,7 +1,5 @@ package net.corda.nodeapi -import com.google.common.annotations.VisibleForTesting -import com.google.common.net.HostAndPort import net.corda.core.crypto.toBase58String import net.corda.core.messaging.MessageRecipientGroup import net.corda.core.messaging.MessageRecipients @@ -11,6 +9,7 @@ import net.corda.core.node.services.ServiceType import net.corda.core.read import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.core.utilities.NetworkHostAndPort import net.corda.nodeapi.config.SSLConfiguration import java.security.KeyStore import java.security.PublicKey @@ -43,11 +42,11 @@ abstract class ArtemisMessagingComponent : SingletonSerializeAsToken() { } interface ArtemisPeerAddress : ArtemisAddress, SingleMessageRecipient { - val hostAndPort: HostAndPort + val hostAndPort: NetworkHostAndPort } @CordaSerializable - data class NetworkMapAddress(override val hostAndPort: HostAndPort) : ArtemisPeerAddress { + data class NetworkMapAddress(override val hostAndPort: NetworkHostAndPort) : ArtemisPeerAddress { override val queueName: String get() = NETWORK_MAP_QUEUE } @@ -63,13 +62,13 @@ abstract class ArtemisMessagingComponent : SingletonSerializeAsToken() { * @param hostAndPort The address of the node. */ @CordaSerializable - data class NodeAddress(override val queueName: String, override val hostAndPort: HostAndPort) : ArtemisPeerAddress { + data class NodeAddress(override val queueName: String, override val hostAndPort: NetworkHostAndPort) : ArtemisPeerAddress { companion object { - fun asPeer(peerIdentity: PublicKey, hostAndPort: HostAndPort): NodeAddress { + fun asPeer(peerIdentity: PublicKey, hostAndPort: NetworkHostAndPort): NodeAddress { return NodeAddress("$PEERS_PREFIX${peerIdentity.toBase58String()}", hostAndPort) } - fun asService(serviceIdentity: PublicKey, hostAndPort: HostAndPort): NodeAddress { + fun asService(serviceIdentity: PublicKey, hostAndPort: NetworkHostAndPort): NodeAddress { return NodeAddress("$SERVICES_PREFIX${serviceIdentity.toBase58String()}", hostAndPort) } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt b/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt index cdd0074a1b..5cda03946f 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt @@ -1,6 +1,6 @@ package net.corda.nodeapi -import com.google.common.net.HostAndPort +import net.corda.core.utilities.NetworkHostAndPort import net.corda.nodeapi.config.SSLConfiguration import org.apache.activemq.artemis.api.core.TransportConfiguration import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnectorFactory @@ -36,7 +36,7 @@ class ArtemisTcpTransport { fun tcpTransport( direction: ConnectionDirection, - hostAndPort: HostAndPort, + hostAndPort: NetworkHostAndPort, config: SSLConfiguration?, enableSSL: Boolean = true ): TransportConfiguration { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/config/ConfigUtilities.kt b/node-api/src/main/kotlin/net/corda/nodeapi/config/ConfigUtilities.kt index 9726136af0..f7cf998d7e 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/config/ConfigUtilities.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/config/ConfigUtilities.kt @@ -1,9 +1,10 @@ package net.corda.nodeapi.config -import com.google.common.net.HostAndPort import com.typesafe.config.Config import com.typesafe.config.ConfigUtil import net.corda.core.noneOrSingle +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.parseNetworkHostAndPort import org.bouncycastle.asn1.x500.X500Name import org.slf4j.LoggerFactory import java.net.Proxy @@ -67,7 +68,7 @@ private fun Config.getSingleValue(path: String, type: KType): Any? { Boolean::class -> getBoolean(path) LocalDate::class -> LocalDate.parse(getString(path)) Instant::class -> Instant.parse(getString(path)) - HostAndPort::class -> HostAndPort.fromString(getString(path)) + NetworkHostAndPort::class -> getString(path).parseNetworkHostAndPort() Path::class -> Paths.get(getString(path)) URL::class -> URL(getString(path)) Properties::class -> getConfig(path).toProperties() @@ -95,7 +96,7 @@ private fun Config.getCollectionValue(path: String, type: KType): Collection getBooleanList(path) LocalDate::class -> getStringList(path).map(LocalDate::parse) Instant::class -> getStringList(path).map(Instant::parse) - HostAndPort::class -> getStringList(path).map(HostAndPort::fromString) + NetworkHostAndPort::class -> getStringList(path).map { it.parseNetworkHostAndPort() } Path::class -> getStringList(path).map { Paths.get(it) } URL::class -> getStringList(path).map(::URL) X500Name::class -> getStringList(path).map(::X500Name) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/serialization/DefaultWhitelist.kt b/node-api/src/main/kotlin/net/corda/nodeapi/serialization/DefaultWhitelist.kt index 31e9743eee..9e5b507cdb 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/serialization/DefaultWhitelist.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/serialization/DefaultWhitelist.kt @@ -1,9 +1,9 @@ package net.corda.nodeapi.serialization import com.esotericsoftware.kryo.KryoException -import com.google.common.net.HostAndPort import net.corda.core.node.CordaPluginRegistry import net.corda.core.serialization.SerializationCustomization +import net.corda.core.utilities.NetworkHostAndPort import org.apache.activemq.artemis.api.core.SimpleString import rx.Notification import rx.exceptions.OnErrorNotImplementedException @@ -33,7 +33,7 @@ class DefaultWhitelist : CordaPluginRegistry() { addToWhitelist(listOf(Unit).javaClass) // SingletonList addToWhitelist(setOf(Unit).javaClass) // SingletonSet addToWhitelist(mapOf(Unit to Unit).javaClass) // SingletonSet - addToWhitelist(HostAndPort::class.java) + addToWhitelist(NetworkHostAndPort::class.java) addToWhitelist(SimpleString::class.java) addToWhitelist(KryoException::class.java) addToWhitelist(StringBuffer::class.java) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/config/ConfigParsingTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/config/ConfigParsingTest.kt index 3a09140194..511eee6527 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/config/ConfigParsingTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/config/ConfigParsingTest.kt @@ -1,11 +1,11 @@ package net.corda.nodeapi.config -import com.google.common.net.HostAndPort import com.typesafe.config.Config import com.typesafe.config.ConfigFactory.empty import com.typesafe.config.ConfigRenderOptions.defaults import com.typesafe.config.ConfigValueFactory import net.corda.core.div +import net.corda.core.utilities.NetworkHostAndPort import net.corda.testing.getTestX509Name import org.assertj.core.api.Assertions.assertThat import org.bouncycastle.asn1.x500.X500Name @@ -59,10 +59,10 @@ class ConfigParsingTest { } @Test - fun `HostAndPort`() { - testPropertyType( - HostAndPort.fromParts("localhost", 2223), - HostAndPort.fromParts("localhost", 2225), + fun `NetworkHostAndPort`() { + testPropertyType( + NetworkHostAndPort("localhost", 2223), + NetworkHostAndPort("localhost", 2225), valuesToString = true) } @@ -223,8 +223,8 @@ class ConfigParsingTest { data class LocalDateListData(override val values: List) : ListData data class InstantData(override val value: Instant) : SingleData data class InstantListData(override val values: List) : ListData - data class HostAndPortData(override val value: HostAndPort) : SingleData - data class HostAndPortListData(override val values: List) : ListData + data class NetworkHostAndPortData(override val value: NetworkHostAndPort) : SingleData + data class NetworkHostAndPortListData(override val values: List) : ListData data class PathData(override val value: Path) : SingleData data class PathListData(override val values: List) : ListData data class URLData(override val value: URL) : SingleData diff --git a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt index d0aeb6547b..e996f9d43b 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt @@ -1,6 +1,5 @@ package net.corda.node.services -import com.google.common.net.HostAndPort import com.nhaarman.mockito_kotlin.whenever import net.corda.core.ErrorOr import net.corda.core.contracts.ContractState @@ -13,6 +12,7 @@ import net.corda.core.div import net.corda.core.getOrThrow import net.corda.core.identity.Party import net.corda.core.node.services.ServiceInfo +import net.corda.core.utilities.NetworkHostAndPort import net.corda.flows.NotaryError import net.corda.flows.NotaryException import net.corda.flows.NotaryFlow @@ -53,7 +53,7 @@ class BFTNotaryServiceTests { serviceType.id, clusterName) val bftNotaryService = ServiceInfo(serviceType, clusterName) - val notaryClusterAddresses = replicaIds.map { HostAndPort.fromParts("localhost", 11000 + it * 10) } + val notaryClusterAddresses = replicaIds.map { NetworkHostAndPort("localhost", 11000 + it * 10) } replicaIds.forEach { replicaId -> mockNet.createNode( node.network.myAddress, diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt index 3b99f6845f..9cae274260 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt @@ -1,7 +1,6 @@ package net.corda.services.messaging import co.paralleluniverse.fibers.Suspendable -import com.google.common.net.HostAndPort import net.corda.client.rpc.CordaRPCClient import net.corda.core.crypto.generateKeyPair import net.corda.core.crypto.toBase58String @@ -11,6 +10,7 @@ import net.corda.core.flows.InitiatingFlow import net.corda.core.getOrThrow import net.corda.core.identity.Party import net.corda.core.messaging.CordaRPCOps +import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.crypto.random63BitValue import net.corda.testing.ALICE import net.corda.testing.BOB @@ -144,13 +144,13 @@ abstract class MQSecurityTest : NodeBasedTest() { assertAllQueueCreationAttacksFail(randomQueue) } - fun clientTo(target: HostAndPort, sslConfiguration: SSLConfiguration? = configureTestSSL()): SimpleMQClient { + fun clientTo(target: NetworkHostAndPort, sslConfiguration: SSLConfiguration? = configureTestSSL()): SimpleMQClient { val client = SimpleMQClient(target, sslConfiguration) clients += client return client } - fun loginToRPC(target: HostAndPort, rpcUser: User, sslConfiguration: SSLConfiguration? = null): CordaRPCOps { + fun loginToRPC(target: NetworkHostAndPort, rpcUser: User, sslConfiguration: SSLConfiguration? = null): CordaRPCOps { return CordaRPCClient(target, sslConfiguration).start(rpcUser.username, rpcUser.password).proxy } diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 2e754c518c..2f8d586e10 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -3,7 +3,6 @@ package net.corda.node.internal import com.codahale.metrics.MetricRegistry import com.google.common.annotations.VisibleForTesting import com.google.common.collect.MutableClassToInstanceMap -import com.google.common.net.HostAndPort import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.MoreExecutors import com.google.common.util.concurrent.SettableFuture @@ -24,6 +23,7 @@ import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.deserialize import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.debug import net.corda.flows.* import net.corda.node.services.* @@ -621,7 +621,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, } /** Return list of node's addresses. It's overridden in MockNetwork as we don't have real addresses for MockNodes. */ - protected abstract fun myAddresses(): List + protected abstract fun myAddresses(): List /** This is overriden by the mock node implementation to enable operation without any network map service */ protected open fun noNetworkMapConfigured(): ListenableFuture { diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index 871bdd6307..c9ec5fcac3 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -1,7 +1,6 @@ package net.corda.node.internal import com.codahale.metrics.JmxReporter -import com.google.common.net.HostAndPort import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.SettableFuture @@ -12,7 +11,9 @@ import net.corda.core.node.ServiceHub import net.corda.core.node.services.ServiceInfo import net.corda.core.seconds import net.corda.core.success +import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.parseNetworkHostAndPort import net.corda.core.utilities.trace import net.corda.node.VersionInfo import net.corda.node.serialization.NodeClock @@ -155,21 +156,21 @@ open class Node(override val configuration: FullNodeConfiguration, advertisedAddress) } - private fun makeLocalMessageBroker(): HostAndPort { + private fun makeLocalMessageBroker(): NetworkHostAndPort { with(configuration) { messageBroker = ArtemisMessagingServer(this, p2pAddress.port, rpcAddress?.port, services.networkMapCache, userService) - return HostAndPort.fromParts("localhost", p2pAddress.port) + return NetworkHostAndPort("localhost", p2pAddress.port) } } - private fun getAdvertisedAddress(): HostAndPort { + private fun getAdvertisedAddress(): NetworkHostAndPort { return with(configuration) { val useHost = if (detectPublicIp) { tryDetectIfNotPublicHost(p2pAddress.host) ?: p2pAddress.host } else { p2pAddress.host } - HostAndPort.fromParts(useHost, p2pAddress.port) + NetworkHostAndPort(useHost, p2pAddress.port) } } @@ -201,7 +202,7 @@ open class Node(override val configuration: FullNodeConfiguration, * it back to the queue. * - Once the message is received the session is closed and the queue deleted. */ - private fun discoverPublicHost(serverAddress: HostAndPort): String? { + private fun discoverPublicHost(serverAddress: NetworkHostAndPort): String? { log.trace { "Trying to detect public hostname through the Network Map Service at $serverAddress" } val tcpTransport = ArtemisTcpTransport.tcpTransport(ConnectionDirection.Outbound(), serverAddress, configuration) val locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport).apply { @@ -227,14 +228,14 @@ open class Node(override val configuration: FullNodeConfiguration, val consumer = session.createConsumer(queueName) val artemisMessage: ClientMessage = consumer.receive(10.seconds.toMillis()) ?: throw IOException("Did not receive a response from the Network Map Service at $serverAddress") - val publicHostAndPort = HostAndPort.fromString(artemisMessage.getStringProperty(ipDetectResponseProperty)) + val publicHostAndPort = artemisMessage.getStringProperty(ipDetectResponseProperty) log.info("Detected public address: $publicHostAndPort") consumer.close() session.deleteQueue(queueName) clientFactory.close() - return publicHostAndPort.host.removePrefix("/") + return publicHostAndPort.removePrefix("/").parseNetworkHostAndPort().host } override fun startMessagingService(rpcOps: RPCOps) { @@ -257,7 +258,7 @@ open class Node(override val configuration: FullNodeConfiguration, return networkMapConnection.flatMap { super.registerWithNetworkMap() } } - override fun myAddresses(): List { + override fun myAddresses(): List { val address = network.myAddress as ArtemisMessagingComponent.ArtemisPeerAddress return listOf(address.hostAndPort) } @@ -359,4 +360,4 @@ open class Node(override val configuration: FullNodeConfiguration, class ConfigurationException(message: String) : Exception(message) -data class NetworkMapInfo(val address: HostAndPort, val legalName: X500Name) +data class NetworkMapInfo(val address: NetworkHostAndPort, val legalName: X500Name) diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index a052e20968..da56b1e2fd 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -1,7 +1,7 @@ package net.corda.node.services.config -import com.google.common.net.HostAndPort import net.corda.core.node.services.ServiceInfo +import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.internal.NetworkMapInfo import net.corda.node.services.messaging.CertificateChainCheckPolicy import net.corda.node.services.network.NetworkMapService @@ -27,8 +27,8 @@ interface NodeConfiguration : NodeSSLConfiguration { val verifierType: VerifierType val messageRedeliveryDelaySeconds: Int val bftReplicaId: Int? - val notaryNodeAddress: HostAndPort? - val notaryClusterAddresses: List + val notaryNodeAddress: NetworkHostAndPort? + val notaryClusterAddresses: List } data class FullNodeConfiguration( @@ -50,15 +50,15 @@ data class FullNodeConfiguration( override val messageRedeliveryDelaySeconds: Int = 30, val useHTTPS: Boolean, @OldConfig("artemisAddress") - val p2pAddress: HostAndPort, - val rpcAddress: HostAndPort?, + val p2pAddress: NetworkHostAndPort, + val rpcAddress: NetworkHostAndPort?, // TODO This field is slightly redundant as p2pAddress is sufficient to hold the address of the node's MQ broker. // Instead this should be a Boolean indicating whether that broker is an internal one started by the node or an external one - val messagingServerAddress: HostAndPort?, + val messagingServerAddress: NetworkHostAndPort?, val extraAdvertisedServiceIds: List, override val bftReplicaId: Int?, - override val notaryNodeAddress: HostAndPort?, - override val notaryClusterAddresses: List, + override val notaryNodeAddress: NetworkHostAndPort?, + override val notaryClusterAddresses: List, override val certificateChainCheckPolicies: List, override val devMode: Boolean = false, val useTestClock: Boolean = false, diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt index cf5214e217..66b2da18f6 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt @@ -1,6 +1,5 @@ package net.corda.node.services.messaging -import com.google.common.net.HostAndPort import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.SettableFuture import io.netty.handler.ssl.SslHandler @@ -11,6 +10,7 @@ import net.corda.core.crypto.X509Utilities.CORDA_ROOT_CA import net.corda.core.node.NodeInfo import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.NetworkMapCache.MapChange +import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.debug import net.corda.core.utilities.loggerFor import net.corda.node.internal.Node @@ -376,7 +376,7 @@ class ArtemisMessagingServer(override val config: NodeConfiguration, } private fun createTcpTransport(connectionDirection: ConnectionDirection, host: String, port: Int, enableSSL: Boolean = true) = - ArtemisTcpTransport.tcpTransport(connectionDirection, HostAndPort.fromParts(host, port), config, enableSSL = enableSSL) + ArtemisTcpTransport.tcpTransport(connectionDirection, NetworkHostAndPort(host, port), config, enableSSL = enableSSL) /** * All nodes are expected to have a public facing address called [ArtemisMessagingComponent.P2P_QUEUE] for receiving @@ -384,7 +384,7 @@ class ArtemisMessagingServer(override val config: NodeConfiguration, * as defined by ArtemisAddress.queueName. A bridge is then created to forward messages from this queue to the node's * P2P address. */ - private fun deployBridge(queueName: String, target: HostAndPort, legalName: X500Name) { + private fun deployBridge(queueName: String, target: NetworkHostAndPort, legalName: X500Name) { val connectionDirection = ConnectionDirection.Outbound( connectorFactoryClassName = VerifyingNettyConnectorFactory::class.java.name, expectedCommonName = legalName @@ -420,7 +420,7 @@ class ArtemisMessagingServer(override val config: NodeConfiguration, private val ArtemisPeerAddress.bridgeName: String get() = getBridgeName(queueName, hostAndPort) - private fun getBridgeName(queueName: String, hostAndPort: HostAndPort): String = "$queueName -> $hostAndPort" + private fun getBridgeName(queueName: String, hostAndPort: NetworkHostAndPort): String = "$queueName -> $hostAndPort" // This is called on one of Artemis' background threads internal fun hostVerificationFail(expectedLegalName: X500Name, errorMsg: String?) { diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt index beaa4ddc12..713c1be649 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt @@ -1,6 +1,5 @@ package net.corda.node.services.messaging -import com.google.common.net.HostAndPort import com.google.common.util.concurrent.ListenableFuture import net.corda.core.* import net.corda.core.crypto.random63BitValue @@ -12,6 +11,7 @@ import net.corda.core.node.services.PartyInfo import net.corda.core.node.services.TransactionVerifierService import net.corda.core.utilities.opaque import net.corda.core.transactions.LedgerTransaction +import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.loggerFor import net.corda.core.utilities.trace import net.corda.node.VersionInfo @@ -71,13 +71,13 @@ import javax.annotation.concurrent.ThreadSafe @ThreadSafe class NodeMessagingClient(override val config: NodeConfiguration, val versionInfo: VersionInfo, - val serverAddress: HostAndPort, + val serverAddress: NetworkHostAndPort, val myIdentity: PublicKey?, val nodeExecutor: AffinityExecutor.ServiceAffinityExecutor, val database: Database, val networkMapRegistrationFuture: ListenableFuture, val monitoringService: MonitoringService, - advertisedAddress: HostAndPort = serverAddress + advertisedAddress: NetworkHostAndPort = serverAddress ) : ArtemisMessagingComponent(), MessagingService { companion object { private val log = loggerFor() diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRtConfig.kt b/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRtConfig.kt index c11952c202..72c0212eb2 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRtConfig.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRtConfig.kt @@ -1,7 +1,7 @@ package net.corda.node.services.transactions -import com.google.common.net.HostAndPort import net.corda.core.div +import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.debug import net.corda.core.utilities.loggerFor import java.io.FileWriter @@ -17,14 +17,14 @@ import java.util.concurrent.TimeUnit.MILLISECONDS * Each instance of this class creates such a configHome, accessible via [path]. * The files are deleted on [close] typically via [use], see [PathManager] for details. */ -class BFTSMaRtConfig(private val replicaAddresses: List, debug: Boolean = false) : PathManager(Files.createTempDirectory("bft-smart-config")) { +class BFTSMaRtConfig(private val replicaAddresses: List, debug: Boolean = false) : PathManager(Files.createTempDirectory("bft-smart-config")) { companion object { private val log = loggerFor() internal val portIsClaimedFormat = "Port %s is claimed by another replica: %s" } init { - val claimedPorts = mutableSetOf() + val claimedPorts = mutableSetOf() val n = replicaAddresses.size (0 until n).forEach { replicaId -> // Each replica claims the configured port and the next one: @@ -66,7 +66,7 @@ class BFTSMaRtConfig(private val replicaAddresses: List, debug: Boo log.debug { "Replica $peerId is ready for P2P." } } - private fun replicaPorts(replicaId: Int): List { + private fun replicaPorts(replicaId: Int): List { val base = replicaAddresses[replicaId] return BFTSMaRtPort.values().map { it.ofReplica(base) } } @@ -76,10 +76,10 @@ private enum class BFTSMaRtPort(private val off: Int) { FOR_CLIENTS(0), FOR_REPLICAS(1); - fun ofReplica(base: HostAndPort) = HostAndPort.fromParts(base.host, base.port + off) + fun ofReplica(base: NetworkHostAndPort) = NetworkHostAndPort(base.host, base.port + off) } -private fun HostAndPort.isListening() = try { +private fun NetworkHostAndPort.isListening() = try { Socket(host, port).use { true } // Will cause one error to be logged in the replica on success. } catch (e: SocketException) { false diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt index e0f514cc32..b12c7ce477 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt @@ -1,6 +1,5 @@ package net.corda.node.services.transactions -import com.google.common.net.HostAndPort import io.atomix.catalyst.buffer.BufferInput import io.atomix.catalyst.buffer.BufferOutput import io.atomix.catalyst.serializer.Serializer @@ -48,13 +47,13 @@ class RaftUniquenessProvider(services: ServiceHubInternal) : UniquenessProvider, /** Directory storing the Raft log and state machine snapshots */ private val storagePath: Path = services.configuration.baseDirectory /** Address of the Copycat node run by this Corda node */ - private val myAddress: HostAndPort = services.configuration.notaryNodeAddress + private val myAddress = services.configuration.notaryNodeAddress ?: throw IllegalArgumentException("notaryNodeAddress must be specified in configuration") /** * List of node addresses in the existing Copycat cluster. At least one active node must be * provided to join the cluster. If empty, a new cluster will be bootstrapped. */ - private val clusterAddresses: List = services.configuration.notaryClusterAddresses + private val clusterAddresses = services.configuration.notaryClusterAddresses /** The database to store the state machine state in */ private val db: Database = services.database /** SSL configuration */ diff --git a/node/src/main/kotlin/net/corda/node/utilities/ConfigUtils.kt b/node/src/main/kotlin/net/corda/node/utilities/ConfigUtils.kt deleted file mode 100644 index 5048cb8981..0000000000 --- a/node/src/main/kotlin/net/corda/node/utilities/ConfigUtils.kt +++ /dev/null @@ -1,9 +0,0 @@ -package net.corda.node.utilities - -import com.google.common.net.HostAndPort -import com.typesafe.config.Config -import java.nio.file.Path -import java.nio.file.Paths - -fun Config.getHostAndPort(name: String): HostAndPort = HostAndPort.fromString(getString(name)) -fun Config.getPath(name: String): Path = Paths.get(getString(name)) \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt index 3b8988ee0a..4cc7883c43 100644 --- a/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt @@ -1,7 +1,7 @@ package net.corda.node.services.config -import com.google.common.net.HostAndPort import net.corda.core.crypto.commonName +import net.corda.core.utilities.NetworkHostAndPort import net.corda.testing.ALICE import net.corda.nodeapi.User import net.corda.testing.node.makeTestDataSourceProperties @@ -25,8 +25,8 @@ class FullNodeConfigurationTest { rpcUsers = emptyList(), verifierType = VerifierType.InMemory, useHTTPS = false, - p2pAddress = HostAndPort.fromParts("localhost", 0), - rpcAddress = HostAndPort.fromParts("localhost", 1), + p2pAddress = NetworkHostAndPort("localhost", 0), + rpcAddress = NetworkHostAndPort("localhost", 1), messagingServerAddress = null, extraAdvertisedServiceIds = emptyList(), bftReplicaId = null, diff --git a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt index b93802356b..d92bec8a22 100644 --- a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt @@ -1,13 +1,14 @@ package net.corda.node.services.messaging import com.codahale.metrics.MetricRegistry -import com.google.common.net.HostAndPort import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.SettableFuture import net.corda.core.crypto.generateKeyPair import net.corda.core.messaging.RPCOps import net.corda.core.node.services.DEFAULT_SESSION_ID +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.testing.ALICE import net.corda.testing.LogHelper import net.corda.node.services.RPCUserService import net.corda.node.services.RPCUserServiceImpl @@ -20,7 +21,6 @@ import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction -import net.corda.testing.ALICE import net.corda.testing.freeLocalHostAndPort import net.corda.testing.freePort import net.corda.testing.node.MOCK_VERSION_INFO @@ -218,7 +218,7 @@ class ArtemisMessagingTests { return messagingClient } - private fun createMessagingClient(server: HostAndPort = HostAndPort.fromParts("localhost", serverPort)): NodeMessagingClient { + private fun createMessagingClient(server: NetworkHostAndPort = NetworkHostAndPort("localhost", serverPort)): NodeMessagingClient { return database.transaction { NodeMessagingClient( config, diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/BFTSMaRtConfigTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/BFTSMaRtConfigTests.kt index 0dd0af75dc..5fdee29f25 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/BFTSMaRtConfigTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/BFTSMaRtConfigTests.kt @@ -1,6 +1,6 @@ package net.corda.node.services.transactions -import com.google.common.net.HostAndPort +import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.services.transactions.BFTSMaRtConfig.Companion.portIsClaimedFormat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Test @@ -28,7 +28,7 @@ class BFTSMaRtConfigTests { @Test fun `overlapping port ranges are rejected`() { - fun addresses(vararg ports: Int) = ports.map { HostAndPort.fromParts("localhost", it) } + fun addresses(vararg ports: Int) = ports.map { NetworkHostAndPort("localhost", it) } assertThatThrownBy { BFTSMaRtConfig(addresses(11000, 11001)).use {} } .isInstanceOf(IllegalArgumentException::class.java) .hasMessage(portIsClaimedFormat.format("localhost:11001", setOf("localhost:11000", "localhost:11001"))) diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/DistributedImmutableMapTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/DistributedImmutableMapTests.kt index fe57ded32c..7895f4defc 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/DistributedImmutableMapTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/DistributedImmutableMapTests.kt @@ -1,6 +1,5 @@ package net.corda.node.services.transactions -import com.google.common.net.HostAndPort import io.atomix.catalyst.transport.Address import io.atomix.copycat.client.ConnectionStrategies import io.atomix.copycat.client.CopycatClient @@ -8,6 +7,7 @@ import io.atomix.copycat.server.CopycatServer import io.atomix.copycat.server.storage.Storage import io.atomix.copycat.server.storage.StorageLevel import net.corda.core.getOrThrow +import net.corda.core.utilities.NetworkHostAndPort import net.corda.testing.LogHelper import net.corda.node.services.network.NetworkMapService import net.corda.node.utilities.configureDatabase @@ -87,7 +87,7 @@ class DistributedImmutableMapTests { return cluster.map { it.getOrThrow() } } - private fun createReplica(myAddress: HostAndPort, clusterAddress: HostAndPort? = null): CompletableFuture { + private fun createReplica(myAddress: NetworkHostAndPort, clusterAddress: NetworkHostAndPort? = null): CompletableFuture { val storage = Storage.builder().withStorageLevel(StorageLevel.MEMORY).build() val address = Address(myAddress.host, myAddress.port) diff --git a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt index f66506808e..22f54033f3 100644 --- a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt +++ b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt @@ -1,7 +1,6 @@ package net.corda.attachmentdemo import co.paralleluniverse.fibers.Suspendable -import com.google.common.net.HostAndPort import joptsimple.OptionParser import net.corda.client.rpc.CordaRPCClient import net.corda.core.contracts.Contract @@ -53,14 +52,14 @@ fun main(args: Array) { val role = options.valueOf(roleArg)!! when (role) { Role.SENDER -> { - val host = HostAndPort.fromString("localhost:10006") + val host = NetworkHostAndPort("localhost", 10006) println("Connecting to sender node ($host)") CordaRPCClient(host).start("demo", "demo").use { sender(it.proxy) } } Role.RECIPIENT -> { - val host = HostAndPort.fromString("localhost:10009") + val host = NetworkHostAndPort("localhost", 10009) println("Connecting to the recipient node ($host)") CordaRPCClient(host).start("demo", "demo").use { recipient(it.proxy) diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt index 3f9622d293..1517af2424 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt @@ -1,12 +1,12 @@ package net.corda.bank -import com.google.common.net.HostAndPort import joptsimple.OptionParser import net.corda.bank.api.BankOfCordaClientApi import net.corda.bank.api.BankOfCordaWebApi.IssueRequestParams import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceType import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.NetworkHostAndPort import net.corda.testing.DUMMY_NOTARY import net.corda.flows.CashExitFlow import net.corda.flows.CashPaymentFlow @@ -72,13 +72,13 @@ private class BankOfCordaDriver { when (role) { Role.ISSUE_CASH_RPC -> { println("Requesting Cash via RPC ...") - val result = BankOfCordaClientApi(HostAndPort.fromString("localhost:10006")).requestRPCIssue(requestParams) + val result = BankOfCordaClientApi(NetworkHostAndPort("localhost", 10006)).requestRPCIssue(requestParams) if (result is SignedTransaction) println("Success!! You transaction receipt is ${result.tx.id}") } Role.ISSUE_CASH_WEB -> { println("Requesting Cash via Web ...") - val result = BankOfCordaClientApi(HostAndPort.fromString("localhost:10007")).requestWebIssue(requestParams) + val result = BankOfCordaClientApi(NetworkHostAndPort("localhost", 10007)).requestWebIssue(requestParams) if (result) println("Successfully processed Cash Issue request") } diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt index 7c5563d323..f8bf5df778 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt @@ -1,6 +1,5 @@ package net.corda.bank.api -import com.google.common.net.HostAndPort import net.corda.bank.api.BankOfCordaWebApi.IssueRequestParams import net.corda.client.rpc.CordaRPCClient import net.corda.core.contracts.Amount @@ -9,13 +8,14 @@ import net.corda.core.getOrThrow import net.corda.core.messaging.startFlow import net.corda.core.utilities.OpaqueBytes import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.NetworkHostAndPort import net.corda.flows.IssuerFlow.IssuanceRequester import net.corda.testing.http.HttpApi /** * Interface for communicating with Bank of Corda node */ -class BankOfCordaClientApi(val hostAndPort: HostAndPort) { +class BankOfCordaClientApi(val hostAndPort: NetworkHostAndPort) { private val apiRoot = "api/bank" /** * HTTP API diff --git a/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt b/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt index ce8cfee139..ac93b3ec0b 100644 --- a/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt +++ b/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt @@ -1,11 +1,11 @@ package net.corda.irs -import com.google.common.net.HostAndPort import com.google.common.util.concurrent.Futures import net.corda.client.rpc.CordaRPCClient import net.corda.core.getOrThrow import net.corda.core.node.services.ServiceInfo import net.corda.core.toFuture +import net.corda.core.utilities.NetworkHostAndPort import net.corda.testing.DUMMY_BANK_A import net.corda.testing.DUMMY_BANK_B import net.corda.testing.DUMMY_NOTARY @@ -101,7 +101,7 @@ class IRSDemoTest : IntegrationTestCategory { assertThat(nodeApi.postJson("deals", tradeFile)).isTrue() } - private fun runUploadRates(host: HostAndPort) { + private fun runUploadRates(host: NetworkHostAndPort) { println("Running upload rates against $host") val fileContents = loadResourceFile("net/corda/irs/simulation/example.rates.txt") val url = URL("http://$host/api/irs/fixes") diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/IRSDemo.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/IRSDemo.kt index fa4a2f020a..5b9f023933 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/IRSDemo.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/IRSDemo.kt @@ -2,8 +2,8 @@ package net.corda.irs -import com.google.common.net.HostAndPort import joptsimple.OptionParser +import net.corda.core.utilities.NetworkHostAndPort import kotlin.system.exitProcess enum class Role { @@ -29,9 +29,9 @@ fun main(args: Array) { val role = options.valueOf(roleArg)!! val value = options.valueOf(valueArg) when (role) { - Role.UploadRates -> IRSDemoClientApi(HostAndPort.fromString("localhost:10004")).runUploadRates() - Role.Trade -> IRSDemoClientApi(HostAndPort.fromString("localhost:10007")).runTrade(value) - Role.Date -> IRSDemoClientApi(HostAndPort.fromString("localhost:10010")).runDateChange(value) + Role.UploadRates -> IRSDemoClientApi(NetworkHostAndPort("localhost", 10004)).runUploadRates() + Role.Trade -> IRSDemoClientApi(NetworkHostAndPort("localhost", 10007)).runTrade(value) + Role.Date -> IRSDemoClientApi(NetworkHostAndPort("localhost", 10010)).runDateChange(value) } } diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/IrsDemoClientApi.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/IrsDemoClientApi.kt index c983115559..31b3ccf13b 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/IrsDemoClientApi.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/IrsDemoClientApi.kt @@ -1,6 +1,6 @@ package net.corda.irs -import com.google.common.net.HostAndPort +import net.corda.core.utilities.NetworkHostAndPort import net.corda.irs.utilities.uploadFile import net.corda.testing.http.HttpApi import org.apache.commons.io.IOUtils @@ -9,7 +9,7 @@ import java.net.URL /** * Interface for communicating with nodes running the IRS demo. */ -class IRSDemoClientApi(private val hostAndPort: HostAndPort) { +class IRSDemoClientApi(private val hostAndPort: NetworkHostAndPort) { private val api = HttpApi.fromHostAndPort(hostAndPort, apiRoot) fun runTrade(tradeId: String): Boolean { diff --git a/samples/notary-demo/src/main/kotlin/net/corda/demorun/util/DemoUtils.kt b/samples/notary-demo/src/main/kotlin/net/corda/demorun/util/DemoUtils.kt index 45d25fdb43..6aa3cc1aa4 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/demorun/util/DemoUtils.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/demorun/util/DemoUtils.kt @@ -1,9 +1,9 @@ package net.corda.demorun.util -import com.google.common.net.HostAndPort import net.corda.cordform.CordformDefinition import net.corda.cordform.CordformNode import net.corda.core.node.services.ServiceInfo +import net.corda.core.utilities.NetworkHostAndPort import net.corda.nodeapi.User import org.bouncycastle.asn1.x500.X500Name @@ -21,6 +21,6 @@ fun CordformNode.advertisedServices(vararg services: ServiceInfo) { advertisedServices = services.map { it.toString() } } -fun CordformNode.notaryClusterAddresses(vararg addresses: HostAndPort) { +fun CordformNode.notaryClusterAddresses(vararg addresses: NetworkHostAndPort) { notaryClusterAddresses = addresses.map { it.toString() } } diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt index 8abca6e424..3f6af7fc9f 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt @@ -1,6 +1,5 @@ package net.corda.notarydemo -import com.google.common.net.HostAndPort import net.corda.core.div import net.corda.core.node.services.ServiceInfo import net.corda.testing.ALICE @@ -14,6 +13,7 @@ import net.corda.cordform.CordformContext import net.corda.cordform.CordformNode import net.corda.core.stream import net.corda.core.toTypedArray +import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.services.transactions.minCorrectReplicas import org.bouncycastle.asn1.x500.X500Name @@ -38,7 +38,7 @@ object BFTNotaryCordform : CordformDefinition("build" / "notary-demo-nodes", not p2pPort(10005) rpcPort(10006) } - val clusterAddresses = (0 until clusterSize).stream().mapToObj { HostAndPort.fromParts("localhost", 11000 + it * 10) }.toTypedArray() + val clusterAddresses = (0 until clusterSize).stream().mapToObj { NetworkHostAndPort("localhost", 11000 + it * 10) }.toTypedArray() fun notaryNode(replicaId: Int, configure: CordformNode.() -> Unit) = node { name(notaryNames[replicaId]) advertisedServices(advertisedService) diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt index f85c9846b2..855fd1dd27 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt @@ -1,6 +1,5 @@ package net.corda.notarydemo -import com.google.common.net.HostAndPort import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import net.corda.client.rpc.CordaRPCClient @@ -11,13 +10,14 @@ import net.corda.core.map import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.startFlow import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.NetworkHostAndPort import net.corda.testing.BOB import net.corda.notarydemo.flows.DummyIssueAndMove import net.corda.notarydemo.flows.RPCStartableNotaryFlowClient import kotlin.streams.asSequence fun main(args: Array) { - val address = HostAndPort.fromParts("localhost", 10003) + val address = NetworkHostAndPort("localhost", 10003) println("Connecting to the recipient node ($address)") CordaRPCClient(address).start(notaryDemoUser.username, notaryDemoUser.password).use { NotaryDemoClientApi(it.proxy).notarise(10) diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt index 1915d9ff69..10a94ea6a4 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt @@ -1,6 +1,5 @@ package net.corda.notarydemo -import com.google.common.net.HostAndPort import net.corda.core.crypto.appendToCommonName import net.corda.core.div import net.corda.core.node.services.ServiceInfo @@ -13,6 +12,7 @@ import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.cordform.CordformDefinition import net.corda.cordform.CordformContext import net.corda.cordform.CordformNode +import net.corda.core.utilities.NetworkHostAndPort import net.corda.demorun.runNodes import net.corda.demorun.util.node import org.bouncycastle.asn1.x500.X500Name @@ -49,7 +49,7 @@ object RaftNotaryCordform : CordformDefinition("build" / "notary-demo-nodes", no p2pPort(10009) rpcPort(10010) } - val clusterAddress = HostAndPort.fromParts("localhost", 10008) // Otherwise each notary forms its own cluster. + val clusterAddress = NetworkHostAndPort("localhost", 10008) // Otherwise each notary forms its own cluster. notaryNode(1) { notaryNodePort(10012) p2pPort(10013) diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemo.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemo.kt index bc84fdb459..7fba92d602 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemo.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemo.kt @@ -1,9 +1,9 @@ package net.corda.traderdemo -import com.google.common.net.HostAndPort import joptsimple.OptionParser import net.corda.client.rpc.CordaRPCClient import net.corda.core.contracts.DOLLARS +import net.corda.core.utilities.NetworkHostAndPort import net.corda.testing.DUMMY_BANK_A import net.corda.core.utilities.loggerFor import org.slf4j.Logger @@ -42,12 +42,12 @@ private class TraderDemo { // will contact the buyer and actually make something happen. val role = options.valueOf(roleArg)!! if (role == Role.BUYER) { - val host = HostAndPort.fromString("localhost:10006") + val host = NetworkHostAndPort("localhost", 10006) CordaRPCClient(host).start("demo", "demo").use { TraderDemoClientApi(it.proxy).runBuyer() } } else { - val host = HostAndPort.fromString("localhost:10009") + val host = NetworkHostAndPort("localhost", 10009) CordaRPCClient(host).use("demo", "demo") { TraderDemoClientApi(it.proxy).runSeller(1000.DOLLARS, DUMMY_BANK_A.name) } diff --git a/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt b/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt index c939bd4758..f7a56f2be9 100644 --- a/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt +++ b/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt @@ -1,10 +1,10 @@ package net.corda.smoketesting -import com.google.common.net.HostAndPort import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.CordaRPCConnection import net.corda.core.createDirectories import net.corda.core.div +import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.loggerFor import java.nio.file.Path import java.nio.file.Paths @@ -60,7 +60,7 @@ class NodeProcess( confFile.writeText(config.toText()) val process = startNode(nodeDir) - val client = CordaRPCClient(HostAndPort.fromParts("localhost", config.rpcPort)) + val client = CordaRPCClient(NetworkHostAndPort("localhost", config.rpcPort)) val user = config.users[0] val setupExecutor = Executors.newSingleThreadScheduledExecutor() diff --git a/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt b/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt index bffece9c57..6c36d638c6 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt @@ -3,7 +3,6 @@ package net.corda.testing -import com.google.common.net.HostAndPort import com.nhaarman.mockito_kotlin.spy import com.nhaarman.mockito_kotlin.whenever import net.corda.core.contracts.StateRef @@ -14,6 +13,7 @@ import net.corda.core.node.ServiceHub import net.corda.core.node.services.IdentityService import net.corda.core.utilities.OpaqueBytes import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.VerifierType import net.corda.node.services.config.configureDevKeyAndTrustStores @@ -88,7 +88,7 @@ val ALL_TEST_KEYS: List get() = listOf(MEGA_CORP_KEY, MINI_CORP_KEY, AL val MOCK_IDENTITIES = listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_NOTARY_IDENTITY) val MOCK_IDENTITY_SERVICE: IdentityService get() = InMemoryIdentityService(MOCK_IDENTITIES, emptyMap(), DUMMY_CA.certificate.cert) -val MOCK_HOST_AND_PORT = HostAndPort.fromParts("mockHost", 30000) +val MOCK_HOST_AND_PORT = NetworkHostAndPort("mockHost", 30000) fun generateStateRef() = StateRef(SecureHash.randomSHA256(), 0) @@ -99,7 +99,7 @@ private val freePortCounter = AtomicInteger(30000) * Unsafe for getting multiple ports! * Use [getFreeLocalPorts] for getting multiple ports. */ -fun freeLocalHostAndPort(): HostAndPort = HostAndPort.fromParts("localhost", freePort()) +fun freeLocalHostAndPort() = NetworkHostAndPort("localhost", freePort()) /** * Returns a free port. @@ -115,9 +115,9 @@ fun freePort(): Int = freePortCounter.getAndAccumulate(0) { prev, _ -> 30000 + ( * Unlikely, but in the time between running this function and handing the ports * to the Node, some other process else could allocate the returned ports. */ -fun getFreeLocalPorts(hostName: String, numberToAlloc: Int): List { +fun getFreeLocalPorts(hostName: String, numberToAlloc: Int): List { val freePort = freePortCounter.getAndAccumulate(0) { prev, _ -> 30000 + (prev - 30000 + numberToAlloc) % 10000 } - return (freePort .. freePort + numberToAlloc - 1).map { HostAndPort.fromParts(hostName, it) } + return (freePort .. freePort + numberToAlloc - 1).map { NetworkHostAndPort(hostName, it) } } /** diff --git a/test-utils/src/main/kotlin/net/corda/testing/NodeApi.kt b/test-utils/src/main/kotlin/net/corda/testing/NodeApi.kt deleted file mode 100644 index c812beec84..0000000000 --- a/test-utils/src/main/kotlin/net/corda/testing/NodeApi.kt +++ /dev/null @@ -1,62 +0,0 @@ -package net.corda.testing - -import com.google.common.net.HostAndPort -import java.io.IOException -import java.io.InputStreamReader -import java.net.ConnectException -import java.net.HttpURLConnection -import java.net.SocketException -import java.net.URL -import kotlin.test.assertEquals - -class NodeApi { - class NodeDidNotStartException(message: String) : Exception(message) - - companion object { - // Increased timeout to two minutes. - val NODE_WAIT_RETRY_COUNT: Int = 600 - val NODE_WAIT_RETRY_DELAY_MS: Long = 200 - - fun ensureNodeStartsOrKill(proc: Process, nodeWebserverAddr: HostAndPort) { - try { - assertEquals(proc.isAlive, true) - waitForNodeStartup(nodeWebserverAddr) - } catch (e: Throwable) { - println("Forcibly killing node process") - proc.destroyForcibly() - throw e - } - } - - private fun waitForNodeStartup(nodeWebserverAddr: HostAndPort) { - val url = URL("http://$nodeWebserverAddr/api/status") - var retries = 0 - var respCode: Int - do { - retries++ - val err = try { - val conn = url.openConnection() as HttpURLConnection - conn.requestMethod = "GET" - respCode = conn.responseCode - InputStreamReader(conn.inputStream).readLines().joinToString { it } - } catch(e: ConnectException) { - // This is to be expected while it loads up - respCode = 404 - "Node hasn't started" - } catch(e: SocketException) { - respCode = -1 - "Could not connect: $e" - } catch (e: IOException) { - respCode = -1 - "IOException: $e" - } - - if (retries > NODE_WAIT_RETRY_COUNT) { - throw NodeDidNotStartException("The node did not start: " + err) - } - - Thread.sleep(NODE_WAIT_RETRY_DELAY_MS) - } while (respCode != 200) - } - } -} diff --git a/test-utils/src/main/kotlin/net/corda/testing/RPCDriver.kt b/test-utils/src/main/kotlin/net/corda/testing/RPCDriver.kt index 2276f19301..af6844273b 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/RPCDriver.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/RPCDriver.kt @@ -1,6 +1,5 @@ package net.corda.testing -import com.google.common.net.HostAndPort import com.google.common.util.concurrent.ListenableFuture import net.corda.client.mock.Generator import net.corda.client.mock.generateOrFail @@ -11,6 +10,8 @@ import net.corda.client.rpc.internal.RPCClientConfiguration import net.corda.core.div import net.corda.core.map import net.corda.core.messaging.RPCOps +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.parseNetworkHostAndPort import net.corda.core.crypto.random63BitValue import net.corda.testing.driver.ProcessUtilities import net.corda.node.services.RPCUserService @@ -109,7 +110,7 @@ interface RPCDriverExposedDSLInterface : DriverDSLExposedInterface { maxFileSize: Int = ArtemisMessagingServer.MAX_FILE_SIZE, maxBufferedBytesPerClient: Long = 10L * ArtemisMessagingServer.MAX_FILE_SIZE, configuration: RPCServerConfiguration = RPCServerConfiguration.default, - customPort: HostAndPort? = null, + customPort: NetworkHostAndPort? = null, ops : I ) : ListenableFuture @@ -124,7 +125,7 @@ interface RPCDriverExposedDSLInterface : DriverDSLExposedInterface { */ fun startRpcClient( rpcOpsClass: Class, - rpcAddress: HostAndPort, + rpcAddress: NetworkHostAndPort, username: String = rpcTestUser.username, password: String = rpcTestUser.password, configuration: RPCClientConfiguration = RPCClientConfiguration.default @@ -140,7 +141,7 @@ interface RPCDriverExposedDSLInterface : DriverDSLExposedInterface { */ fun startRandomRpcClient( rpcOpsClass: Class, - rpcAddress: HostAndPort, + rpcAddress: NetworkHostAndPort, username: String = rpcTestUser.username, password: String = rpcTestUser.password ): ListenableFuture @@ -153,7 +154,7 @@ interface RPCDriverExposedDSLInterface : DriverDSLExposedInterface { * @param password The password to authenticate with. */ fun startArtemisSession( - rpcAddress: HostAndPort, + rpcAddress: NetworkHostAndPort, username: String = rpcTestUser.username, password: String = rpcTestUser.password ): ClientSession @@ -163,7 +164,7 @@ interface RPCDriverExposedDSLInterface : DriverDSLExposedInterface { rpcUser: User = rpcTestUser, maxFileSize: Int = ArtemisMessagingServer.MAX_FILE_SIZE, maxBufferedBytesPerClient: Long = 10L * ArtemisMessagingServer.MAX_FILE_SIZE, - customPort: HostAndPort? = null + customPort: NetworkHostAndPort? = null ): ListenableFuture fun startInVmRpcBroker( @@ -186,12 +187,12 @@ inline fun RPCDriverExposedDSLInterface.startInVmRpcClient( configuration: RPCClientConfiguration = RPCClientConfiguration.default ) = startInVmRpcClient(I::class.java, username, password, configuration) inline fun RPCDriverExposedDSLInterface.startRandomRpcClient( - hostAndPort: HostAndPort, + hostAndPort: NetworkHostAndPort, username: String = rpcTestUser.username, password: String = rpcTestUser.password ) = startRandomRpcClient(I::class.java, hostAndPort, username, password) inline fun RPCDriverExposedDSLInterface.startRpcClient( - rpcAddress: HostAndPort, + rpcAddress: NetworkHostAndPort, username: String = rpcTestUser.username, password: String = rpcTestUser.password, configuration: RPCClientConfiguration = RPCClientConfiguration.default @@ -200,7 +201,7 @@ inline fun RPCDriverExposedDSLInterface.startRpcClient( interface RPCDriverInternalDSLInterface : DriverDSLInternalInterface, RPCDriverExposedDSLInterface data class RpcBrokerHandle( - val hostAndPort: HostAndPort?, /** null if this is an InVM broker */ + val hostAndPort: NetworkHostAndPort?,/** null if this is an InVM broker */ val clientTransportConfiguration: TransportConfiguration, val serverControl: ActiveMQServerControl ) @@ -306,7 +307,7 @@ data class RPCDriverDSL( configureCommonSettings(maxFileSize, maxBufferedBytesPerClient) } } - fun createRpcServerArtemisConfig(maxFileSize: Int, maxBufferedBytesPerClient: Long, baseDirectory: Path, hostAndPort: HostAndPort): Configuration { + fun createRpcServerArtemisConfig(maxFileSize: Int, maxBufferedBytesPerClient: Long, baseDirectory: Path, hostAndPort: NetworkHostAndPort): Configuration { val connectionDirection = ConnectionDirection.Inbound(acceptorFactoryClassName = NettyAcceptorFactory::class.java.name) return ConfigurationImpl().apply { val artemisDir = "$baseDirectory/artemis" @@ -318,7 +319,7 @@ data class RPCDriverDSL( } } val inVmClientTransportConfiguration = TransportConfiguration(InVMConnectorFactory::class.java.name) - fun createNettyClientTransportConfiguration(hostAndPort: HostAndPort): TransportConfiguration { + fun createNettyClientTransportConfiguration(hostAndPort: NetworkHostAndPort): TransportConfiguration { return ArtemisTcpTransport.tcpTransport(ConnectionDirection.Outbound(), hostAndPort, null) } } @@ -366,7 +367,7 @@ data class RPCDriverDSL( maxFileSize: Int, maxBufferedBytesPerClient: Long, configuration: RPCServerConfiguration, - customPort: HostAndPort?, + customPort: NetworkHostAndPort?, ops: I ): ListenableFuture { return startRpcBroker(serverName, rpcUser, maxFileSize, maxBufferedBytesPerClient, customPort).map { broker -> @@ -376,7 +377,7 @@ data class RPCDriverDSL( override fun startRpcClient( rpcOpsClass: Class, - rpcAddress: HostAndPort, + rpcAddress: NetworkHostAndPort, username: String, password: String, configuration: RPCClientConfiguration @@ -391,7 +392,7 @@ data class RPCDriverDSL( } } - override fun startRandomRpcClient(rpcOpsClass: Class, rpcAddress: HostAndPort, username: String, password: String): ListenableFuture { + override fun startRandomRpcClient(rpcOpsClass: Class, rpcAddress: NetworkHostAndPort, username: String, password: String): ListenableFuture { val processFuture = driverDSL.executorService.submit { ProcessUtilities.startJavaProcess(listOf(rpcOpsClass.name, rpcAddress.toString(), username, password)) } @@ -399,7 +400,7 @@ data class RPCDriverDSL( return processFuture } - override fun startArtemisSession(rpcAddress: HostAndPort, username: String, password: String): ClientSession { + override fun startArtemisSession(rpcAddress: NetworkHostAndPort, username: String, password: String): ClientSession { val locator = ActiveMQClient.createServerLocatorWithoutHA(createNettyClientTransportConfiguration(rpcAddress)) val sessionFactory = locator.createSessionFactory() val session = sessionFactory.createSession(username, password, false, true, true, false, DEFAULT_ACK_BATCH_SIZE) @@ -417,7 +418,7 @@ data class RPCDriverDSL( rpcUser: User, maxFileSize: Int, maxBufferedBytesPerClient: Long, - customPort: HostAndPort? + customPort: NetworkHostAndPort? ): ListenableFuture { val hostAndPort = customPort ?: driverDSL.portAllocation.nextHostAndPort() addressMustNotBeBound(driverDSL.executorService, hostAndPort) @@ -506,7 +507,7 @@ class RandomRpcUser { require(args.size == 4) @Suppress("UNCHECKED_CAST") val rpcClass = Class.forName(args[0]) as Class - val hostAndPort = HostAndPort.fromString(args[1]) + val hostAndPort = args[1].parseNetworkHostAndPort() val username = args[2] val password = args[3] val handle = RPCClient(hostAndPort, null).start(rpcClass, username, password) diff --git a/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt b/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt index 3d21d4c827..c176242b41 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -2,7 +2,6 @@ package net.corda.testing.driver -import com.google.common.net.HostAndPort import com.google.common.util.concurrent.* import com.typesafe.config.Config import com.typesafe.config.ConfigRenderOptions @@ -20,8 +19,10 @@ import net.corda.core.messaging.CordaRPCOps import net.corda.core.node.NodeInfo import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceType +import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.WHITESPACE import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.parseNetworkHostAndPort import net.corda.node.internal.Node import net.corda.node.internal.NodeStartup import net.corda.node.serialization.NodeClock @@ -167,13 +168,13 @@ sealed class NodeHandle { abstract val nodeInfo: NodeInfo abstract val rpc: CordaRPCOps abstract val configuration: FullNodeConfiguration - abstract val webAddress: HostAndPort + abstract val webAddress: NetworkHostAndPort data class OutOfProcess( override val nodeInfo: NodeInfo, override val rpc: CordaRPCOps, override val configuration: FullNodeConfiguration, - override val webAddress: HostAndPort, + override val webAddress: NetworkHostAndPort, val debugPort: Int?, val process: Process ) : NodeHandle() @@ -182,7 +183,7 @@ sealed class NodeHandle { override val nodeInfo: NodeInfo, override val rpc: CordaRPCOps, override val configuration: FullNodeConfiguration, - override val webAddress: HostAndPort, + override val webAddress: NetworkHostAndPort, val node: Node, val nodeThread: Thread ) : NodeHandle() @@ -191,13 +192,13 @@ sealed class NodeHandle { } data class WebserverHandle( - val listenAddress: HostAndPort, + val listenAddress: NetworkHostAndPort, val process: Process ) sealed class PortAllocation { abstract fun nextPort(): Int - fun nextHostAndPort(): HostAndPort = HostAndPort.fromParts("localhost", nextPort()) + fun nextHostAndPort() = NetworkHostAndPort("localhost", nextPort()) class Incremental(startingPort: Int) : PortAllocation() { val portCounter = AtomicInteger(startingPort) @@ -298,16 +299,16 @@ fun getTimestampAsDirectoryName(): String { return DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(UTC).format(Instant.now()) } -class ListenProcessDeathException(hostAndPort: HostAndPort, listenProcess: Process) : Exception("The process that was expected to listen on $hostAndPort has died with status: ${listenProcess.exitValue()}") +class ListenProcessDeathException(hostAndPort: NetworkHostAndPort, listenProcess: Process) : Exception("The process that was expected to listen on $hostAndPort has died with status: ${listenProcess.exitValue()}") /** * @throws ListenProcessDeathException if [listenProcess] dies before the check succeeds, i.e. the check can't succeed as intended. */ -fun addressMustBeBound(executorService: ScheduledExecutorService, hostAndPort: HostAndPort, listenProcess: Process? = null) { +fun addressMustBeBound(executorService: ScheduledExecutorService, hostAndPort: NetworkHostAndPort, listenProcess: Process? = null) { addressMustBeBoundFuture(executorService, hostAndPort, listenProcess).getOrThrow() } -fun addressMustBeBoundFuture(executorService: ScheduledExecutorService, hostAndPort: HostAndPort, listenProcess: Process? = null): ListenableFuture { +fun addressMustBeBoundFuture(executorService: ScheduledExecutorService, hostAndPort: NetworkHostAndPort, listenProcess: Process? = null): ListenableFuture { return poll(executorService, "address $hostAndPort to bind") { if (listenProcess != null && !listenProcess.isAlive) { throw ListenProcessDeathException(hostAndPort, listenProcess) @@ -321,11 +322,11 @@ fun addressMustBeBoundFuture(executorService: ScheduledExecutorService, hostAndP } } -fun addressMustNotBeBound(executorService: ScheduledExecutorService, hostAndPort: HostAndPort) { +fun addressMustNotBeBound(executorService: ScheduledExecutorService, hostAndPort: NetworkHostAndPort) { addressMustNotBeBoundFuture(executorService, hostAndPort).getOrThrow() } -fun addressMustNotBeBoundFuture(executorService: ScheduledExecutorService, hostAndPort: HostAndPort): ListenableFuture { +fun addressMustNotBeBoundFuture(executorService: ScheduledExecutorService, hostAndPort: NetworkHostAndPort): ListenableFuture { return poll(executorService, "address $hostAndPort to unbind") { try { Socket(hostAndPort.host, hostAndPort.port).close() @@ -514,7 +515,7 @@ class DriverDSL( _executorService?.shutdownNow() } - private fun establishRpc(nodeAddress: HostAndPort, sslConfig: SSLConfiguration, processDeathFuture: ListenableFuture): ListenableFuture { + private fun establishRpc(nodeAddress: NetworkHostAndPort, sslConfig: SSLConfiguration, processDeathFuture: ListenableFuture): ListenableFuture { val client = CordaRPCClient(nodeAddress, sslConfig) val connectionFuture = poll(executorService, "RPC connection") { try { @@ -544,9 +545,9 @@ class DriverDSL( } } is NetworkMapStartStrategy.Nominated -> { - serviceConfig(HostAndPort.fromString(networkMapCandidates.filter { + serviceConfig(networkMapCandidates.filter { it.name == legalName.toString() - }.single().config.getString("p2pAddress"))).let { + }.single().config.getString("p2pAddress").parseNetworkHostAndPort()).let { { nodeName: X500Name -> if (nodeName == legalName) null else it } } } @@ -704,7 +705,7 @@ class DriverDSL( return startNodeInternal(config, webAddress, startInProcess) } - private fun startNodeInternal(config: Config, webAddress: HostAndPort, startInProcess: Boolean?): ListenableFuture { + private fun startNodeInternal(config: Config, webAddress: NetworkHostAndPort, startInProcess: Boolean?): ListenableFuture { val nodeConfiguration = config.parseAs() if (startInProcess ?: startNodesInProcess) { val nodeAndThreadFuture = startInProcessNode(executorService, nodeConfiguration, config) diff --git a/test-utils/src/main/kotlin/net/corda/testing/driver/NetworkMapStartStrategy.kt b/test-utils/src/main/kotlin/net/corda/testing/driver/NetworkMapStartStrategy.kt index 62248682ef..e185930198 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/driver/NetworkMapStartStrategy.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/driver/NetworkMapStartStrategy.kt @@ -1,13 +1,13 @@ package net.corda.testing.driver -import com.google.common.net.HostAndPort +import net.corda.core.utilities.NetworkHostAndPort import net.corda.testing.DUMMY_MAP import org.bouncycastle.asn1.x500.X500Name sealed class NetworkMapStartStrategy { internal abstract val startDedicated: Boolean internal abstract val legalName: X500Name - internal fun serviceConfig(address: HostAndPort) = mapOf( + internal fun serviceConfig(address: NetworkHostAndPort) = mapOf( "address" to address.toString(), "legalName" to legalName.toString() ) diff --git a/test-utils/src/main/kotlin/net/corda/testing/http/HttpApi.kt b/test-utils/src/main/kotlin/net/corda/testing/http/HttpApi.kt index 42b3221264..dbbe571af5 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/http/HttpApi.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/http/HttpApi.kt @@ -1,7 +1,7 @@ package net.corda.testing.http import com.fasterxml.jackson.databind.ObjectMapper -import com.google.common.net.HostAndPort +import net.corda.core.utilities.NetworkHostAndPort import java.net.URL class HttpApi(val root: URL, val mapper: ObjectMapper = defaultMapper) { @@ -27,7 +27,7 @@ class HttpApi(val root: URL, val mapper: ObjectMapper = defaultMapper) { private fun toJson(any: Any) = any as? String ?: HttpUtils.defaultMapper.writeValueAsString(any) companion object { - fun fromHostAndPort(hostAndPort: HostAndPort, base: String, protocol: String = "http", mapper: ObjectMapper = defaultMapper): HttpApi + fun fromHostAndPort(hostAndPort: NetworkHostAndPort, base: String, protocol: String = "http", mapper: ObjectMapper = defaultMapper): HttpApi = HttpApi(URL("$protocol://$hostAndPort/$base/"), mapper) private val defaultMapper: ObjectMapper by lazy { net.corda.jackson.JacksonSupport.createNonRpcMapper() diff --git a/test-utils/src/main/kotlin/net/corda/testing/messaging/SimpleMQClient.kt b/test-utils/src/main/kotlin/net/corda/testing/messaging/SimpleMQClient.kt index dd521c8671..12ac044952 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/messaging/SimpleMQClient.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/messaging/SimpleMQClient.kt @@ -1,6 +1,6 @@ package net.corda.testing.messaging -import com.google.common.net.HostAndPort +import net.corda.core.utilities.NetworkHostAndPort import net.corda.nodeapi.ArtemisMessagingComponent import net.corda.nodeapi.ArtemisTcpTransport import net.corda.nodeapi.ConnectionDirection @@ -12,7 +12,7 @@ import org.bouncycastle.asn1.x500.X500Name /** * As the name suggests this is a simple client for connecting to MQ brokers. */ -class SimpleMQClient(val target: HostAndPort, +class SimpleMQClient(val target: NetworkHostAndPort, override val config: SSLConfiguration? = configureTestSSL(DEFAULT_MQ_LEGAL_NAME)) : ArtemisMessagingComponent() { companion object { val DEFAULT_MQ_LEGAL_NAME = X500Name("CN=SimpleMQClient,O=R3,OU=corda,L=London,C=GB") diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt index c4de528dd1..a9b26e75bb 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt @@ -1,12 +1,12 @@ package net.corda.testing.node import co.paralleluniverse.common.util.VisibleForTesting -import com.google.common.net.HostAndPort import net.corda.core.crypto.entropyToKeyPair import net.corda.core.identity.Party import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub import net.corda.core.node.services.NetworkMapCache +import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.services.network.InMemoryNetworkMapCache import net.corda.testing.getTestPartyAndCertificate import net.corda.testing.getTestX509Name @@ -21,8 +21,8 @@ class MockNetworkMapCache(serviceHub: ServiceHub) : InMemoryNetworkMapCache(serv private companion object { val BANK_C = getTestPartyAndCertificate(getTestX509Name("Bank C"), entropyToKeyPair(BigInteger.valueOf(1000)).public) val BANK_D = getTestPartyAndCertificate(getTestX509Name("Bank D"), entropyToKeyPair(BigInteger.valueOf(2000)).public) - val BANK_C_ADDR: HostAndPort = HostAndPort.fromParts("bankC", 8080) - val BANK_D_ADDR: HostAndPort = HostAndPort.fromParts("bankD", 8080) + val BANK_C_ADDR = NetworkHostAndPort("bankC", 8080) + val BANK_D_ADDR = NetworkHostAndPort("bankD", 8080) } override val changed: Observable = PublishSubject.create() diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt index 0dfd96d2fe..aaf1abb4ad 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -2,7 +2,6 @@ package net.corda.testing.node import com.google.common.jimfs.Configuration.unix import com.google.common.jimfs.Jimfs -import com.google.common.net.HostAndPort import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import com.nhaarman.mockito_kotlin.whenever @@ -17,6 +16,7 @@ import net.corda.core.messaging.RPCOps import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.CordaPluginRegistry import net.corda.core.node.ServiceEntry +import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.node.WorldMapLocation import net.corda.core.node.services.IdentityService import net.corda.core.node.services.KeyManagementService @@ -222,7 +222,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, override fun makeTransactionVerifierService() = InMemoryTransactionVerifierService(1) - override fun myAddresses(): List = listOf(HostAndPort.fromHost("mockHost")) + override fun myAddresses() = emptyList() override fun start(): MockNode { super.start() diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt index 804e8520b7..9e1fba3192 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -1,6 +1,5 @@ package net.corda.testing.node -import com.google.common.net.HostAndPort import net.corda.core.contracts.Attachment import net.corda.core.crypto.* import net.corda.core.flows.StateMachineRunId @@ -76,7 +75,7 @@ open class MockServices(vararg val keys: KeyPair) : ServiceHub { override val clock: Clock get() = Clock.systemUTC() override val myInfo: NodeInfo get() { val identity = getTestPartyAndCertificate(MEGA_CORP.name, key.public) - return NodeInfo(listOf(HostAndPort.fromHost("localhost")), identity, setOf(identity), 1) + return NodeInfo(emptyList(), identity, setOf(identity), 1) } override val transactionVerifierService: TransactionVerifierService get() = InMemoryTransactionVerifierService(2) diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt b/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt index 2527ae8e0b..d006e380ac 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt @@ -1,13 +1,13 @@ package net.corda.testing.node import com.codahale.metrics.MetricRegistry -import com.google.common.net.HostAndPort import com.google.common.util.concurrent.SettableFuture import net.corda.core.crypto.commonName import net.corda.core.crypto.generateKeyPair import net.corda.core.messaging.RPCOps import net.corda.core.node.services.IdentityService import net.corda.core.node.services.KeyManagementService +import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.services.RPCUserServiceImpl import net.corda.node.services.api.MonitoringService import net.corda.node.services.config.NodeConfiguration @@ -30,8 +30,8 @@ import kotlin.concurrent.thread * This is a bare-bones node which can only send and receive messages. It doesn't register with a network map service or * any other such task that would make it functional in a network and thus left to the user to do so manually. */ -class SimpleNode(val config: NodeConfiguration, val address: HostAndPort = freeLocalHostAndPort(), - rpcAddress: HostAndPort = freeLocalHostAndPort(), +class SimpleNode(val config: NodeConfiguration, val address: NetworkHostAndPort = freeLocalHostAndPort(), + rpcAddress: NetworkHostAndPort = freeLocalHostAndPort(), trustRoot: X509Certificate) : AutoCloseable { private val databaseWithCloseable: Pair = configureDatabase(config.dataSourceProperties) diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt index c546657e7b..cfec8b3757 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt @@ -1,7 +1,7 @@ package net.corda.demobench.model -import com.google.common.net.HostAndPort import com.typesafe.config.Config +import net.corda.core.utilities.parseNetworkHostAndPort import org.bouncycastle.asn1.x500.X500Name import tornadofx.* import java.io.IOException @@ -46,7 +46,7 @@ class InstallFactory : Controller() { private fun Config.parsePort(path: String): Int { val address = this.getString(path) - val port = HostAndPort.fromString(address).port + val port = address.parseNetworkHostAndPort().port require(nodeController.isPortValid(port), { "Invalid port $port from '$path'." }) return port } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/rpc/NodeRPC.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/rpc/NodeRPC.kt index e3d1f95c13..baeb40ba59 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/rpc/NodeRPC.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/rpc/NodeRPC.kt @@ -1,9 +1,9 @@ package net.corda.demobench.rpc -import com.google.common.net.HostAndPort import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.CordaRPCConnection import net.corda.core.messaging.CordaRPCOps +import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.loggerFor import net.corda.demobench.model.NodeConfig import java.util.* @@ -16,7 +16,7 @@ class NodeRPC(config: NodeConfig, start: (NodeConfig, CordaRPCOps) -> Unit, invo val oneSecond = SECONDS.toMillis(1) } - private val rpcClient = CordaRPCClient(HostAndPort.fromParts("localhost", config.rpcPort)) + private val rpcClient = CordaRPCClient(NetworkHostAndPort("localhost", config.rpcPort)) private var rpcConnection: CordaRPCConnection? = null private val timer = Timer() diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt index 06fcfad0de..904a90f717 100644 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt @@ -3,10 +3,10 @@ package net.corda.demobench.model import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.SerializationFeature -import com.google.common.net.HostAndPort import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigValueFactory import net.corda.core.div +import net.corda.core.utilities.NetworkHostAndPort import net.corda.testing.DUMMY_NOTARY import net.corda.node.internal.NetworkMapInfo import net.corda.node.services.config.FullNodeConfiguration @@ -270,5 +270,5 @@ class NodeConfigTest { users = users ) - private fun localPort(port: Int) = HostAndPort.fromParts("localhost", port) + private fun localPort(port: Int) = NetworkHostAndPort("localhost", port) } diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/LoginView.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/LoginView.kt index df873028a2..ec0752d6c2 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/LoginView.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/LoginView.kt @@ -1,10 +1,10 @@ package net.corda.explorer.views -import com.google.common.net.HostAndPort import javafx.beans.property.SimpleIntegerProperty import javafx.scene.control.* import net.corda.client.jfx.model.NodeMonitorModel import net.corda.client.jfx.model.objectProperty +import net.corda.core.utilities.NetworkHostAndPort import net.corda.explorer.model.SettingsModel import org.controlsfx.dialog.ExceptionDialog import tornadofx.* @@ -27,8 +27,8 @@ class LoginView : View() { private val port by objectProperty(SettingsModel::portProperty) private val fullscreen by objectProperty(SettingsModel::fullscreenProperty) - fun login(host: String?, port: Int, username: String, password: String) { - getModel().register(HostAndPort.fromParts(host, port), username, password) + fun login(host: String, port: Int, username: String, password: String) { + getModel().register(NetworkHostAndPort(host, port), username, password) } fun login() { diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/ConnectionManager.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/ConnectionManager.kt index ef84237cbb..80167f0416 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/ConnectionManager.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/ConnectionManager.kt @@ -1,6 +1,5 @@ package net.corda.loadtest -import com.google.common.net.HostAndPort import com.jcraft.jsch.Buffer import com.jcraft.jsch.Identity import com.jcraft.jsch.IdentityRepository @@ -8,6 +7,7 @@ import com.jcraft.jsch.JSch import com.jcraft.jsch.agentproxy.AgentProxy import com.jcraft.jsch.agentproxy.connector.SSHAgentConnector import com.jcraft.jsch.agentproxy.usocket.JNAUSocketFactory +import net.corda.core.utilities.NetworkHostAndPort import net.corda.testing.driver.PortAllocation import org.slf4j.LoggerFactory import java.util.* @@ -61,7 +61,7 @@ fun setupJSchWithSshAgent(): JSch { } class ConnectionManager(private val jSch: JSch) { - fun connectToNode(remoteNode: RemoteNode, localTunnelAddress: HostAndPort): NodeConnection { + fun connectToNode(remoteNode: RemoteNode, localTunnelAddress: NetworkHostAndPort): NodeConnection { val session = jSch.getSession(remoteNode.sshUserName, remoteNode.hostname, 22) // We don't check the host fingerprints because they may change often session.setConfig("StrictHostKeyChecking", "no") diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/NodeConnection.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/NodeConnection.kt index 55cd113430..cecca567b4 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/NodeConnection.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/NodeConnection.kt @@ -1,6 +1,5 @@ package net.corda.loadtest -import com.google.common.net.HostAndPort import com.google.common.util.concurrent.ListenableFuture import com.jcraft.jsch.ChannelExec import com.jcraft.jsch.Session @@ -9,6 +8,7 @@ import net.corda.client.rpc.CordaRPCConnection import net.corda.core.future import net.corda.core.messaging.CordaRPCOps import net.corda.core.node.NodeInfo +import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.loggerFor import net.corda.nodeapi.internal.addShutdownHook import java.io.ByteArrayOutputStream @@ -22,7 +22,7 @@ import java.io.OutputStream * [doWhileClientStopped], otherwise the RPC link will be broken. * TODO: Auto reconnect has been enable for RPC connection, investigate if we still need [doWhileClientStopped]. */ -class NodeConnection(val remoteNode: RemoteNode, private val jSchSession: Session, private val localTunnelAddress: HostAndPort) : Closeable { +class NodeConnection(val remoteNode: RemoteNode, private val jSchSession: Session, private val localTunnelAddress: NetworkHostAndPort) : Closeable { companion object { val log = loggerFor() } diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt index f8adb129b1..66315907aa 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt @@ -1,6 +1,5 @@ package net.corda.verifier -import com.google.common.net.HostAndPort import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListeningScheduledExecutorService @@ -13,6 +12,7 @@ import net.corda.core.div import net.corda.core.map import net.corda.core.crypto.random63BitValue import net.corda.core.transactions.LedgerTransaction +import net.corda.core.utilities.NetworkHostAndPort import net.corda.testing.driver.ProcessUtilities import net.corda.core.utilities.loggerFor import net.corda.node.services.config.configureDevKeyAndTrustStores @@ -50,7 +50,7 @@ interface VerifierExposedDSLInterface : DriverDSLExposedInterface { fun startVerificationRequestor(name: X500Name): ListenableFuture /** Starts an out of process verifier connected to [address] */ - fun startVerifier(address: HostAndPort): ListenableFuture + fun startVerifier(address: NetworkHostAndPort): ListenableFuture /** * Waits until [number] verifiers are listening for verification requests coming from the Node. Check @@ -106,7 +106,7 @@ data class VerifierHandle( /** A handle for the verification requestor */ data class VerificationRequestorHandle( - val p2pAddress: HostAndPort, + val p2pAddress: NetworkHostAndPort, private val responseAddress: SimpleString, private val session: ClientSession, private val requestProducer: ClientProducer, @@ -143,7 +143,7 @@ data class VerifierDriverDSL( companion object { private val log = loggerFor() - fun createConfiguration(baseDirectory: Path, nodeHostAndPort: HostAndPort): Config { + fun createConfiguration(baseDirectory: Path, nodeHostAndPort: NetworkHostAndPort): Config { return ConfigFactory.parseMap( mapOf( "baseDirectory" to baseDirectory.toString(), @@ -152,7 +152,7 @@ data class VerifierDriverDSL( ) } - fun createVerificationRequestorArtemisConfig(baseDirectory: Path, responseAddress: String, hostAndPort: HostAndPort, sslConfiguration: SSLConfiguration): Configuration { + fun createVerificationRequestorArtemisConfig(baseDirectory: Path, responseAddress: String, hostAndPort: NetworkHostAndPort, sslConfiguration: SSLConfiguration): Configuration { val connectionDirection = ConnectionDirection.Inbound(acceptorFactoryClassName = NettyAcceptorFactory::class.java.name) return ConfigurationImpl().apply { val artemisDir = "$baseDirectory/artemis" @@ -183,7 +183,7 @@ data class VerifierDriverDSL( } } - private fun startVerificationRequestorInternal(name: X500Name, hostAndPort: HostAndPort): VerificationRequestorHandle { + private fun startVerificationRequestorInternal(name: X500Name, hostAndPort: NetworkHostAndPort): VerificationRequestorHandle { val baseDir = driverDSL.driverDirectory / name.commonName val sslConfig = object : NodeSSLConfiguration { override val baseDirectory = baseDir @@ -247,7 +247,7 @@ data class VerifierDriverDSL( ) } - override fun startVerifier(address: HostAndPort): ListenableFuture { + override fun startVerifier(address: NetworkHostAndPort): ListenableFuture { log.info("Starting verifier connecting to address $address") val id = verifierCount.andIncrement val jdwpPort = if (driverDSL.isDebug) driverDSL.debugPortAllocation.nextPort() else null diff --git a/verifier/src/main/kotlin/net/corda/verifier/Verifier.kt b/verifier/src/main/kotlin/net/corda/verifier/Verifier.kt index a42b495072..ea54765408 100644 --- a/verifier/src/main/kotlin/net/corda/verifier/Verifier.kt +++ b/verifier/src/main/kotlin/net/corda/verifier/Verifier.kt @@ -1,12 +1,12 @@ package net.corda.verifier -import com.google.common.net.HostAndPort import com.typesafe.config.Config import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigParseOptions import net.corda.core.ErrorOr import net.corda.nodeapi.internal.addShutdownHook import net.corda.core.div +import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.debug import net.corda.core.utilities.loggerFor import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport @@ -23,7 +23,7 @@ data class VerifierConfiguration( override val baseDirectory: Path, val config: Config ) : NodeSSLConfiguration { - val nodeHostAndPort: HostAndPort by config + val nodeHostAndPort: NetworkHostAndPort by config override val keyStorePassword: String by config override val trustStorePassword: String by config } diff --git a/webserver/src/integration-test/kotlin/net/corda/webserver/WebserverDriverTests.kt b/webserver/src/integration-test/kotlin/net/corda/webserver/WebserverDriverTests.kt index 446545944c..dca27b20f1 100644 --- a/webserver/src/integration-test/kotlin/net/corda/webserver/WebserverDriverTests.kt +++ b/webserver/src/integration-test/kotlin/net/corda/webserver/WebserverDriverTests.kt @@ -1,7 +1,7 @@ package net.corda.webserver -import com.google.common.net.HostAndPort import net.corda.core.getOrThrow +import net.corda.core.utilities.NetworkHostAndPort import net.corda.testing.DUMMY_BANK_A import net.corda.testing.driver.WebserverHandle import net.corda.testing.driver.addressMustBeBound @@ -19,7 +19,7 @@ class DriverTests { addressMustBeBound(executorService, webserverHandle.listenAddress, webserverHandle.process) } - fun webserverMustBeDown(webserverAddr: HostAndPort) { + fun webserverMustBeDown(webserverAddr: NetworkHostAndPort) { addressMustNotBeBound(executorService, webserverAddr) } } diff --git a/webserver/src/main/kotlin/net/corda/webserver/WebServerConfig.kt b/webserver/src/main/kotlin/net/corda/webserver/WebServerConfig.kt index 194536869e..ab331c347e 100644 --- a/webserver/src/main/kotlin/net/corda/webserver/WebServerConfig.kt +++ b/webserver/src/main/kotlin/net/corda/webserver/WebServerConfig.kt @@ -1,7 +1,7 @@ package net.corda.webserver -import com.google.common.net.HostAndPort import com.typesafe.config.Config +import net.corda.core.utilities.NetworkHostAndPort import net.corda.nodeapi.config.NodeSSLConfiguration import net.corda.nodeapi.config.getValue import java.nio.file.Path @@ -15,6 +15,6 @@ class WebServerConfig(override val baseDirectory: Path, val config: Config) : No val exportJMXto: String get() = "http" val useHTTPS: Boolean by config val myLegalName: String by config - val p2pAddress: HostAndPort by config // TODO: Use RPC port instead of P2P port (RPC requires authentication, P2P does not) - val webAddress: HostAndPort by config + val p2pAddress: NetworkHostAndPort by config // TODO: Use RPC port instead of P2P port (RPC requires authentication, P2P does not) + val webAddress: NetworkHostAndPort by config } From fb0a043485833417c67adcfb0f15ac66fb7f06f2 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Thu, 6 Jul 2017 10:24:42 +0100 Subject: [PATCH 75/97] Moved dummy contracts to test-utils --- .../core/contracts/testing/DummyContractV2.kt | 55 ----------------- .../core/contracts/testing/DummyState.kt | 10 --- .../core/contracts/DummyContractV2Tests.kt | 4 +- .../contracts/TransactionGraphSearchTests.kt | 4 +- .../corda/core/contracts/TransactionTests.kt | 2 +- .../contracts/clauses/VerifyClausesTests.kt | 2 +- .../core/flows/CollectSignaturesFlowTests.kt | 2 +- .../core/flows/ContractUpgradeFlowTest.kt | 4 +- .../core/flows/ResolveTransactionsFlowTest.kt | 2 +- docs/source/changelog.rst | 3 + docs/source/contract-upgrade.rst | 2 +- .../java/net/corda/docs/FlowCookbookJava.java | 16 ++--- .../kotlin/net/corda/docs/FlowCookbook.kt | 16 ++--- .../net/corda/contracts/asset/CashTests.kt | 2 +- .../corda/contracts/asset/ObligationTests.kt | 2 +- .../services/vault/schemas/VaultSchemaTest.kt | 2 +- .../node/services/BFTNotaryServiceTests.kt | 2 +- .../node/services/RaftNotaryServiceTests.kt | 2 +- .../services/vault/VaultQueryJavaTests.java | 2 +- .../corda/node/services/NotaryChangeTests.kt | 2 +- .../database/RequeryConfigurationTest.kt | 2 +- .../services/events/ScheduledFlowTests.kt | 2 +- .../statemachine/FlowFrameworkTests.kt | 2 +- .../transactions/NotaryServiceTests.kt | 2 +- .../ValidatingNotaryServiceTests.kt | 2 +- .../node/services/vault/VaultQueryTests.kt | 4 +- .../node/services/vault/VaultWithCashTest.kt | 2 +- .../notarydemo/flows/DummyIssueAndMove.kt | 2 +- .../testing/TransactionDSLInterpreter.kt | 2 +- .../corda/testing/contracts}/DummyContract.kt | 2 +- .../testing/contracts/DummyContractV2.kt | 61 +++++++++++++++++++ .../testing/contracts}/DummyLinearContract.kt | 35 +++++++---- .../net/corda/testing/contracts/DummyState.kt | 12 ++++ .../corda/testing/contracts/VaultFiller.kt | 1 - .../net/corda/loadtest/tests/NotaryTest.kt | 2 +- .../net/corda/verifier/GeneratedLedger.kt | 2 +- 36 files changed, 145 insertions(+), 126 deletions(-) delete mode 100644 core/src/main/kotlin/net/corda/core/contracts/testing/DummyContractV2.kt delete mode 100644 core/src/main/kotlin/net/corda/core/contracts/testing/DummyState.kt rename {core/src/main/kotlin/net/corda/core/contracts/testing => test-utils/src/main/kotlin/net/corda/testing/contracts}/DummyContract.kt (98%) create mode 100644 test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContractV2.kt rename {core/src/main/kotlin/net/corda/core/contracts/testing => test-utils/src/main/kotlin/net/corda/testing/contracts}/DummyLinearContract.kt (52%) create mode 100644 test-utils/src/main/kotlin/net/corda/testing/contracts/DummyState.kt diff --git a/core/src/main/kotlin/net/corda/core/contracts/testing/DummyContractV2.kt b/core/src/main/kotlin/net/corda/core/contracts/testing/DummyContractV2.kt deleted file mode 100644 index 5e6be3631e..0000000000 --- a/core/src/main/kotlin/net/corda/core/contracts/testing/DummyContractV2.kt +++ /dev/null @@ -1,55 +0,0 @@ -package net.corda.core.contracts.testing - -// The dummy contract doesn't do anything useful. It exists for testing purposes. -val DUMMY_V2_PROGRAM_ID = net.corda.core.contracts.testing.DummyContractV2() - -/** - * Dummy contract state for testing of the upgrade process. - */ -// DOCSTART 1 -class DummyContractV2 : net.corda.core.contracts.UpgradedContract { - override val legacyContract = DummyContract::class.java - - data class State(val magicNumber: Int = 0, val owners: List) : net.corda.core.contracts.ContractState { - override val contract = net.corda.core.contracts.testing.DUMMY_V2_PROGRAM_ID - override val participants: List = owners - } - - interface Commands : net.corda.core.contracts.CommandData { - class Create : net.corda.core.contracts.TypeOnlyCommandData(), net.corda.core.contracts.testing.DummyContractV2.Commands - class Move : net.corda.core.contracts.TypeOnlyCommandData(), net.corda.core.contracts.testing.DummyContractV2.Commands - } - - override fun upgrade(state: DummyContract.State): net.corda.core.contracts.testing.DummyContractV2.State { - return net.corda.core.contracts.testing.DummyContractV2.State(state.magicNumber, state.participants) - } - - override fun verify(tx: net.corda.core.contracts.TransactionForContract) { - if (tx.commands.any { it.value is net.corda.core.contracts.UpgradeCommand }) net.corda.flows.ContractUpgradeFlow.Companion.verify(tx) - // Other verifications. - } - - // The "empty contract" - override val legalContractReference: net.corda.core.crypto.SecureHash = net.corda.core.crypto.SecureHash.Companion.sha256("") - // DOCEND 1 - /** - * Generate an upgrade transaction from [DummyContract]. - * - * Note: This is a convenience helper method used for testing only. - * - * @return a pair of wire transaction, and a set of those who should sign the transaction for it to be valid. - */ - fun generateUpgradeFromV1(vararg states: net.corda.core.contracts.StateAndRef): Pair> { - val notary = states.map { it.state.notary }.single() - require(states.isNotEmpty()) - - val signees: Set = states.flatMap { it.state.data.participants }.distinct().toSet() - return Pair(net.corda.core.contracts.TransactionType.General.Builder(notary).apply { - states.forEach { - addInputState(it) - addOutputState(upgrade(it.state.data)) - addCommand(net.corda.core.contracts.UpgradeCommand(DUMMY_V2_PROGRAM_ID.javaClass), signees.map { it.owningKey }.toList()) - } - }.toWireTransaction(), signees) - } -} diff --git a/core/src/main/kotlin/net/corda/core/contracts/testing/DummyState.kt b/core/src/main/kotlin/net/corda/core/contracts/testing/DummyState.kt deleted file mode 100644 index 6c852bb33d..0000000000 --- a/core/src/main/kotlin/net/corda/core/contracts/testing/DummyState.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.corda.core.contracts.testing - -/** - * Dummy state for use in testing. Not part of any contract, not even the [DummyContract]. - */ -data class DummyState(val magicNumber: Int = 0) : net.corda.core.contracts.ContractState { - override val contract = DUMMY_PROGRAM_ID - override val participants: List - get() = emptyList() -} diff --git a/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt b/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt index 81274dfa52..e059dd2e1e 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt @@ -1,7 +1,7 @@ package net.corda.core.contracts -import net.corda.core.contracts.testing.DummyContract -import net.corda.core.contracts.testing.DummyContractV2 +import net.corda.testing.contracts.DummyContract +import net.corda.testing.contracts.DummyContractV2 import net.corda.core.crypto.SecureHash import net.corda.testing.ALICE import net.corda.testing.DUMMY_NOTARY diff --git a/core/src/test/kotlin/net/corda/core/contracts/TransactionGraphSearchTests.kt b/core/src/test/kotlin/net/corda/core/contracts/TransactionGraphSearchTests.kt index f48e5b920b..8f55aa7317 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/TransactionGraphSearchTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/TransactionGraphSearchTests.kt @@ -1,7 +1,7 @@ package net.corda.core.contracts -import net.corda.core.contracts.testing.DummyContract -import net.corda.core.contracts.testing.DummyState +import net.corda.testing.contracts.DummyContract +import net.corda.testing.contracts.DummyState import net.corda.core.crypto.newSecureRandom import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction diff --git a/core/src/test/kotlin/net/corda/core/contracts/TransactionTests.kt b/core/src/test/kotlin/net/corda/core/contracts/TransactionTests.kt index f4bbe7f3f4..ffb3faeb4c 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/TransactionTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/TransactionTests.kt @@ -1,7 +1,7 @@ package net.corda.core.contracts import net.corda.contracts.asset.DUMMY_CASH_ISSUER_KEY -import net.corda.core.contracts.testing.DummyContract +import net.corda.testing.contracts.DummyContract import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.SecureHash import net.corda.core.crypto.generateKeyPair diff --git a/core/src/test/kotlin/net/corda/core/contracts/clauses/VerifyClausesTests.kt b/core/src/test/kotlin/net/corda/core/contracts/clauses/VerifyClausesTests.kt index 076d7e2854..4627f1baa2 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/clauses/VerifyClausesTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/clauses/VerifyClausesTests.kt @@ -4,7 +4,7 @@ import net.corda.core.contracts.AuthenticatedObject import net.corda.core.contracts.CommandData import net.corda.core.contracts.ContractState import net.corda.core.contracts.TransactionForContract -import net.corda.core.contracts.testing.DummyContract +import net.corda.testing.contracts.DummyContract import net.corda.core.crypto.SecureHash import org.junit.Test import kotlin.test.assertFailsWith diff --git a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt index fdc472ab1e..8bccd7666c 100644 --- a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt @@ -4,7 +4,7 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.Command import net.corda.core.contracts.TransactionType import net.corda.core.contracts.requireThat -import net.corda.core.contracts.testing.DummyContract +import net.corda.testing.contracts.DummyContract import net.corda.core.getOrThrow import net.corda.core.identity.Party import net.corda.core.transactions.SignedTransaction diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt index 3f8cfae6ae..1ee8601e34 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -3,8 +3,8 @@ package net.corda.core.flows import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.asset.Cash import net.corda.core.contracts.* -import net.corda.core.contracts.testing.DummyContract -import net.corda.core.contracts.testing.DummyContractV2 +import net.corda.testing.contracts.DummyContract +import net.corda.testing.contracts.DummyContractV2 import net.corda.core.crypto.SecureHash import net.corda.core.getOrThrow import net.corda.core.identity.AbstractParty diff --git a/core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt index 2336298996..341f9b0d5d 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt @@ -1,6 +1,6 @@ package net.corda.core.flows -import net.corda.core.contracts.testing.DummyContract +import net.corda.testing.contracts.DummyContract import net.corda.core.crypto.SecureHash import net.corda.core.getOrThrow import net.corda.core.identity.Party diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index c2ab1cfba8..fa674479d4 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -20,6 +20,9 @@ UNRELEASED * Mock identity constants used in tests, such as ``ALICE``, ``BOB``, ``DUMMY_NOTARY``, have moved to ``net.corda.testing`` in the ``test-utils`` module. +* ``DummyContract``, ``DummyContractV2``, ``DummyLinearContract`` and ``DummyState`` have moved to ``net.corda.testing.contracts`` + in the ``test-utils`` modules. + * In Java, ``QueryCriteriaUtilsKt`` has moved to ``QueryCriteriaUtils``. Also ``and`` and ``or`` are now instance methods of ``QueryCrtieria``. diff --git a/docs/source/contract-upgrade.rst b/docs/source/contract-upgrade.rst index e448ade570..5d7298b36b 100644 --- a/docs/source/contract-upgrade.rst +++ b/docs/source/contract-upgrade.rst @@ -86,7 +86,7 @@ Bank A and Bank B decided to upgrade the contract to ``DummyContractV2`` 1. Developer will create a new contract extending the ``UpgradedContract`` class, and a new state object ``DummyContractV2.State`` referencing the new contract. -.. literalinclude:: /../../core/src/main/kotlin/net/corda/core/contracts/DummyContractV2.kt +.. literalinclude:: /../../test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContractV2.kt :language: kotlin :start-after: DOCSTART 1 :end-before: DOCEND 1 diff --git a/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java b/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java index 8d5d16364d..cf188f4e2c 100644 --- a/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java +++ b/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java @@ -7,8 +7,8 @@ import net.corda.contracts.asset.Cash; import net.corda.core.contracts.*; import net.corda.core.contracts.TransactionType.General; import net.corda.core.contracts.TransactionType.NotaryChange; -import net.corda.core.contracts.testing.DummyContract; -import net.corda.core.contracts.testing.DummyState; +import net.corda.testing.contracts.DummyContract; +import net.corda.testing.contracts.DummyState; import net.corda.core.crypto.DigitalSignature; import net.corda.core.crypto.SecureHash; import net.corda.core.flows.*; @@ -239,8 +239,8 @@ public class FlowCookbookJava { // When building a transaction, input states are passed in as // ``StateRef`` instances, which pair the hash of the transaction // that generated the state with the state's index in the outputs - // of that transaction. In practice, we'd pass the transaction hash - // or the ``StateRef`` as a parameter to the flow, or extract the + // of that transaction. In practice, we'd pass the transaction hash + // or the ``StateRef`` as a parameter to the flow, or extract the // ``StateRef`` from our vault. // DOCSTART 20 StateRef ourStateRef = new StateRef(SecureHash.sha256("DummyTransactionHash"), 0); @@ -381,10 +381,10 @@ public class FlowCookbookJava { // DOCEND 39 // We can also generate a signature over the transaction without - // adding it to the transaction itself. We may do this when - // sending just the signature in a flow instead of returning the - // entire transaction with our signature. This way, the receiving - // node does not need to check we haven't changed anything in the + // adding it to the transaction itself. We may do this when + // sending just the signature in a flow instead of returning the + // entire transaction with our signature. This way, the receiving + // node does not need to check we haven't changed anything in the // transaction. // DOCSTART 40 DigitalSignature.WithKey sig = getServiceHub().createSignature(onceSignedTx); diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt index 14053980cc..6414d50f9b 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt @@ -5,8 +5,8 @@ import net.corda.contracts.asset.Cash import net.corda.core.contracts.* import net.corda.core.contracts.TransactionType.General import net.corda.core.contracts.TransactionType.NotaryChange -import net.corda.core.contracts.testing.DummyContract -import net.corda.core.contracts.testing.DummyState +import net.corda.testing.contracts.DummyContract +import net.corda.testing.contracts.DummyState import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.SecureHash import net.corda.core.flows.* @@ -222,8 +222,8 @@ object FlowCookbook { // When building a transaction, input states are passed in as // ``StateRef`` instances, which pair the hash of the transaction // that generated the state with the state's index in the outputs - // of that transaction. In practice, we'd pass the transaction hash - // or the ``StateRef`` as a parameter to the flow, or extract the + // of that transaction. In practice, we'd pass the transaction hash + // or the ``StateRef`` as a parameter to the flow, or extract the // ``StateRef`` from our vault. // DOCSTART 20 val ourStateRef: StateRef = StateRef(SecureHash.sha256("DummyTransactionHash"), 0) @@ -362,10 +362,10 @@ object FlowCookbook { // DOCEND 39 // We can also generate a signature over the transaction without - // adding it to the transaction itself. We may do this when - // sending just the signature in a flow instead of returning the - // entire transaction with our signature. This way, the receiving - // node does not need to check we haven't changed anything in the + // adding it to the transaction itself. We may do this when + // sending just the signature in a flow instead of returning the + // entire transaction with our signature. This way, the receiving + // node does not need to check we haven't changed anything in the // transaction. // DOCSTART 40 val sig: DigitalSignature.WithKey = serviceHub.createSignature(onceSignedTx) diff --git a/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt b/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt index d08d9e36c4..5a0771ed3d 100644 --- a/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt @@ -2,7 +2,7 @@ package net.corda.contracts.asset import net.corda.testing.contracts.fillWithSomeTestCash import net.corda.core.contracts.* -import net.corda.core.contracts.testing.DummyState +import net.corda.testing.contracts.DummyState import net.corda.core.crypto.SecureHash import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.AbstractParty diff --git a/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt b/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt index 78f2a65759..5d393f71db 100644 --- a/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt @@ -4,7 +4,7 @@ import net.corda.contracts.Commodity import net.corda.contracts.NetType import net.corda.contracts.asset.Obligation.Lifecycle import net.corda.core.contracts.* -import net.corda.core.contracts.testing.DummyState +import net.corda.testing.contracts.DummyState import net.corda.core.crypto.SecureHash import net.corda.core.hours import net.corda.core.crypto.testing.NULL_PARTY diff --git a/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt b/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt index b597a8188d..879f7dd5f0 100644 --- a/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt +++ b/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt @@ -8,7 +8,7 @@ import io.requery.rx.KotlinRxEntityStore import io.requery.sql.* import io.requery.sql.platform.Generic import net.corda.core.contracts.* -import net.corda.core.contracts.testing.DummyContract +import net.corda.testing.contracts.DummyContract import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.SecureHash import net.corda.core.crypto.generateKeyPair diff --git a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt index e996f9d43b..0f8a0c40e4 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt @@ -5,7 +5,7 @@ import net.corda.core.ErrorOr import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateRef import net.corda.core.contracts.TransactionType -import net.corda.core.contracts.testing.DummyContract +import net.corda.testing.contracts.DummyContract import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.SecureHash import net.corda.core.div diff --git a/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt index 1075e83ad2..566c4674e1 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt @@ -4,7 +4,7 @@ import com.google.common.util.concurrent.Futures import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef import net.corda.core.contracts.TransactionType -import net.corda.core.contracts.testing.DummyContract +import net.corda.testing.contracts.DummyContract import net.corda.core.getOrThrow import net.corda.core.identity.Party import net.corda.core.map diff --git a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java index e2ffa4359d..d88d023b5c 100644 --- a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java +++ b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java @@ -5,7 +5,7 @@ import kotlin.Pair; import net.corda.contracts.DealState; import net.corda.contracts.asset.Cash; import net.corda.core.contracts.*; -import net.corda.core.contracts.testing.DummyLinearContract; +import net.corda.testing.contracts.DummyLinearContract; import net.corda.core.crypto.*; import net.corda.core.identity.AbstractParty; import net.corda.core.messaging.DataFeed; diff --git a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt index b47ee4d0a0..8aef7f445e 100644 --- a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt @@ -1,7 +1,7 @@ package net.corda.node.services import net.corda.core.contracts.* -import net.corda.core.contracts.testing.DummyContract +import net.corda.testing.contracts.DummyContract import net.corda.core.crypto.generateKeyPair import net.corda.core.getOrThrow import net.corda.core.identity.Party diff --git a/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt index b0f5f06787..ec8f6a57d2 100644 --- a/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt @@ -5,7 +5,7 @@ import io.requery.kotlin.eq import io.requery.sql.KotlinEntityDataStore import net.corda.core.contracts.StateRef import net.corda.core.contracts.TransactionType -import net.corda.core.contracts.testing.DummyContract +import net.corda.testing.contracts.DummyContract import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.SecureHash import net.corda.core.crypto.testing.NullPublicKey diff --git a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt index 37b26f174f..6fec905bae 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt @@ -2,7 +2,7 @@ package net.corda.node.services.events import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.* -import net.corda.core.contracts.testing.DummyContract +import net.corda.testing.contracts.DummyContract import net.corda.core.crypto.containsAny import net.corda.core.flows.FlowInitiator import net.corda.core.flows.FlowLogic diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt index 26ea5b06f3..f83de31b69 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt @@ -8,7 +8,7 @@ import net.corda.core.* import net.corda.core.contracts.ContractState import net.corda.core.contracts.DOLLARS import net.corda.core.contracts.StateAndRef -import net.corda.core.contracts.testing.DummyState +import net.corda.testing.contracts.DummyState import net.corda.core.crypto.SecureHash import net.corda.core.crypto.generateKeyPair import net.corda.core.crypto.random63BitValue diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt index a553fb8b26..c0da57821c 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt @@ -4,7 +4,7 @@ import com.google.common.util.concurrent.ListenableFuture import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef import net.corda.core.contracts.TransactionType -import net.corda.core.contracts.testing.DummyContract +import net.corda.testing.contracts.DummyContract import net.corda.core.crypto.DigitalSignature import net.corda.core.getOrThrow import net.corda.core.node.services.ServiceInfo diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt index 05bfb68c69..3072677dfd 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt @@ -5,7 +5,7 @@ import net.corda.core.contracts.Command import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef import net.corda.core.contracts.TransactionType -import net.corda.core.contracts.testing.DummyContract +import net.corda.testing.contracts.DummyContract import net.corda.core.crypto.DigitalSignature import net.corda.core.getOrThrow import net.corda.core.node.services.ServiceInfo diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index 1d2fbd7cbf..0ff47da943 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -6,7 +6,7 @@ import net.corda.contracts.DealState import net.corda.contracts.asset.Cash import net.corda.contracts.asset.DUMMY_CASH_ISSUER import net.corda.core.contracts.* -import net.corda.core.contracts.testing.DummyLinearContract +import net.corda.testing.contracts.DummyLinearContract import net.corda.core.crypto.entropyToKeyPair import net.corda.core.crypto.toBase58String import net.corda.core.days @@ -841,7 +841,7 @@ class VaultQueryTests { } assertThat(states).hasSize(20) - assertThat(metadata.first().contractStateClassName).isEqualTo("net.corda.core.contracts.testing.DummyLinearContract\$State") + assertThat(metadata.first().contractStateClassName).isEqualTo("net.corda.testing.contracts.DummyLinearContract\$State") assertThat(metadata.first().status).isEqualTo(Vault.StateStatus.UNCONSUMED) // 0 = UNCONSUMED assertThat(metadata.last().contractStateClassName).isEqualTo("net.corda.contracts.DummyDealContract\$State") assertThat(metadata.last().status).isEqualTo(Vault.StateStatus.CONSUMED) // 1 = CONSUMED diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt index 50314208d4..02686c72b7 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt @@ -7,7 +7,7 @@ import net.corda.testing.contracts.fillWithSomeTestCash import net.corda.testing.contracts.fillWithSomeTestDeals import net.corda.testing.contracts.fillWithSomeTestLinearStates import net.corda.core.contracts.* -import net.corda.core.contracts.testing.DummyLinearContract +import net.corda.testing.contracts.DummyLinearContract import net.corda.core.identity.AnonymousParty import net.corda.core.node.services.VaultService import net.corda.core.node.services.consumedStates diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt index bfd75f00d5..50f51bc9e1 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt @@ -1,7 +1,7 @@ package net.corda.notarydemo.flows import co.paralleluniverse.fibers.Suspendable -import net.corda.core.contracts.testing.DummyContract +import net.corda.testing.contracts.DummyContract import net.corda.core.flows.FlowLogic import net.corda.core.flows.StartableByRPC import net.corda.core.identity.Party diff --git a/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt b/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt index bb660ac058..cb973e022f 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt @@ -1,7 +1,7 @@ package net.corda.testing import net.corda.core.contracts.* -import net.corda.core.contracts.testing.DummyContract +import net.corda.testing.contracts.DummyContract import net.corda.core.crypto.SecureHash import net.corda.core.identity.Party import net.corda.core.seconds diff --git a/core/src/main/kotlin/net/corda/core/contracts/testing/DummyContract.kt b/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContract.kt similarity index 98% rename from core/src/main/kotlin/net/corda/core/contracts/testing/DummyContract.kt rename to test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContract.kt index 5edc0d4f19..e7a96d9a0b 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/testing/DummyContract.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContract.kt @@ -1,4 +1,4 @@ -package net.corda.core.contracts.testing +package net.corda.testing.contracts import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash diff --git a/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContractV2.kt b/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContractV2.kt new file mode 100644 index 0000000000..b14b55937f --- /dev/null +++ b/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContractV2.kt @@ -0,0 +1,61 @@ +package net.corda.testing.contracts + +import net.corda.core.contracts.* +import net.corda.core.crypto.SecureHash +import net.corda.core.identity.AbstractParty +import net.corda.core.transactions.WireTransaction +import net.corda.flows.ContractUpgradeFlow + +// The dummy contract doesn't do anything useful. It exists for testing purposes. +val DUMMY_V2_PROGRAM_ID = DummyContractV2() + +/** + * Dummy contract state for testing of the upgrade process. + */ +// DOCSTART 1 +class DummyContractV2 : UpgradedContract { + override val legacyContract = DummyContract::class.java + + data class State(val magicNumber: Int = 0, val owners: List) : ContractState { + override val contract = DUMMY_V2_PROGRAM_ID + override val participants: List = owners + } + + interface Commands : CommandData { + class Create : TypeOnlyCommandData(), Commands + class Move : TypeOnlyCommandData(), Commands + } + + override fun upgrade(state: DummyContract.State): State { + return State(state.magicNumber, state.participants) + } + + override fun verify(tx: TransactionForContract) { + if (tx.commands.any { it.value is UpgradeCommand }) ContractUpgradeFlow.verify(tx) + // Other verifications. + } + + // The "empty contract" + override val legalContractReference: SecureHash = SecureHash.sha256("") + // DOCEND 1 + /** + * Generate an upgrade transaction from [DummyContract]. + * + * Note: This is a convenience helper method used for testing only. + * + * @return a pair of wire transaction, and a set of those who should sign the transaction for it to be valid. + */ + fun generateUpgradeFromV1(vararg states: StateAndRef): Pair> { + val notary = states.map { it.state.notary }.single() + require(states.isNotEmpty()) + + val signees: Set = states.flatMap { it.state.data.participants }.distinct().toSet() + return Pair(TransactionType.General.Builder(notary).apply { + states.forEach { + addInputState(it) + addOutputState(upgrade(it.state.data)) + addCommand(UpgradeCommand(DUMMY_V2_PROGRAM_ID.javaClass), signees.map { it.owningKey }.toList()) + } + }.toWireTransaction(), signees) + } +} diff --git a/core/src/main/kotlin/net/corda/core/contracts/testing/DummyLinearContract.kt b/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyLinearContract.kt similarity index 52% rename from core/src/main/kotlin/net/corda/core/contracts/testing/DummyLinearContract.kt rename to test-utils/src/main/kotlin/net/corda/testing/contracts/DummyLinearContract.kt index 2723de3a61..46519730cf 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/testing/DummyLinearContract.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyLinearContract.kt @@ -1,36 +1,45 @@ -package net.corda.core.contracts.testing +package net.corda.testing.contracts -import net.corda.core.contracts.CommandData +import net.corda.core.contracts.* +import net.corda.core.contracts.clauses.Clause import net.corda.core.contracts.clauses.FilterOn +import net.corda.core.contracts.clauses.verifyClause +import net.corda.core.crypto.SecureHash import net.corda.core.crypto.containsAny +import net.corda.core.identity.AbstractParty +import net.corda.core.schemas.MappedSchema +import net.corda.core.schemas.PersistentState +import net.corda.core.schemas.QueryableState import net.corda.core.schemas.testing.DummyLinearStateSchemaV1 import net.corda.core.schemas.testing.DummyLinearStateSchemaV2 +import java.time.LocalDateTime +import java.time.ZoneOffset.UTC -class DummyLinearContract : net.corda.core.contracts.Contract { - override val legalContractReference: net.corda.core.crypto.SecureHash = net.corda.core.crypto.SecureHash.Companion.sha256("Test") +class DummyLinearContract : Contract { + override val legalContractReference: SecureHash = SecureHash.sha256("Test") - val clause: net.corda.core.contracts.clauses.Clause = net.corda.core.contracts.LinearState.ClauseVerifier() - override fun verify(tx: net.corda.core.contracts.TransactionForContract) = net.corda.core.contracts.clauses.verifyClause(tx, + val clause: Clause = LinearState.ClauseVerifier() + override fun verify(tx: TransactionForContract) = verifyClause(tx, FilterOn(clause, { states -> states.filterIsInstance() }), emptyList()) data class State( - override val linearId: net.corda.core.contracts.UniqueIdentifier = net.corda.core.contracts.UniqueIdentifier(), - override val contract: net.corda.core.contracts.Contract = net.corda.core.contracts.testing.DummyLinearContract(), - override val participants: List = listOf(), + override val linearId: UniqueIdentifier = UniqueIdentifier(), + override val contract: Contract = DummyLinearContract(), + override val participants: List = listOf(), val linearString: String = "ABC", val linearNumber: Long = 123L, - val linearTimestamp: java.time.Instant = java.time.LocalDateTime.now().toInstant(java.time.ZoneOffset.UTC), + val linearTimestamp: java.time.Instant = LocalDateTime.now().toInstant(UTC), val linearBoolean: Boolean = true, - val nonce: net.corda.core.crypto.SecureHash = net.corda.core.crypto.SecureHash.Companion.randomSHA256()) : net.corda.core.contracts.LinearState, net.corda.core.schemas.QueryableState { + val nonce: SecureHash = SecureHash.randomSHA256()) : LinearState, QueryableState { override fun isRelevant(ourKeys: Set): Boolean { return participants.any { it.owningKey.containsAny(ourKeys) } } - override fun supportedSchemas(): Iterable = listOf(DummyLinearStateSchemaV1, DummyLinearStateSchemaV2) + override fun supportedSchemas(): Iterable = listOf(DummyLinearStateSchemaV1, DummyLinearStateSchemaV2) - override fun generateMappedObject(schema: net.corda.core.schemas.MappedSchema): net.corda.core.schemas.PersistentState { + override fun generateMappedObject(schema: MappedSchema): PersistentState { return when (schema) { is DummyLinearStateSchemaV1 -> DummyLinearStateSchemaV1.PersistentDummyLinearState( externalId = linearId.externalId, diff --git a/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyState.kt b/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyState.kt new file mode 100644 index 0000000000..c2e0696889 --- /dev/null +++ b/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyState.kt @@ -0,0 +1,12 @@ +package net.corda.testing.contracts + +import net.corda.core.contracts.ContractState +import net.corda.core.identity.AbstractParty + +/** + * Dummy state for use in testing. Not part of any contract, not even the [DummyContract]. + */ +data class DummyState(val magicNumber: Int = 0) : ContractState { + override val contract = DUMMY_PROGRAM_ID + override val participants: List get() = emptyList() +} diff --git a/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt b/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt index 9ee4f513c2..0c39d2746e 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt @@ -7,7 +7,6 @@ import net.corda.contracts.DealState import net.corda.contracts.DummyDealContract import net.corda.contracts.asset.* import net.corda.core.contracts.* -import net.corda.core.contracts.testing.DummyLinearContract import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt index 5638dc8e1b..9c3486538d 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt @@ -6,7 +6,7 @@ import net.corda.client.mock.pickOne import net.corda.client.mock.replicate import net.corda.contracts.asset.DUMMY_CASH_ISSUER import net.corda.contracts.asset.DUMMY_CASH_ISSUER_KEY -import net.corda.core.contracts.testing.DummyContract +import net.corda.testing.contracts.DummyContract import net.corda.core.flows.FlowException import net.corda.core.messaging.startFlow import net.corda.core.success diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt index 8962c5d230..fafc621c2e 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt @@ -2,7 +2,7 @@ package net.corda.verifier import net.corda.client.mock.* import net.corda.core.contracts.* -import net.corda.core.contracts.testing.DummyContract +import net.corda.testing.contracts.DummyContract import net.corda.core.crypto.SecureHash import net.corda.core.crypto.X509Utilities import net.corda.core.crypto.entropyToKeyPair From 74c8346863733f0e439589a6b31b133ced6bd381 Mon Sep 17 00:00:00 2001 From: Clinton Date: Fri, 7 Jul 2017 15:37:28 +0100 Subject: [PATCH 76/97] Cordapps now exclude the META-INF of dependencies. (#988) * Cordapps now exclude the META-INF of dependencies. * Only exclude files that cause issues with signed JAR detection. --- constants.properties | 2 +- .../main/groovy/net/corda/plugins/Cordformation.groovy | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/constants.properties b/constants.properties index eebbf86a14..6993620217 100644 --- a/constants.properties +++ b/constants.properties @@ -1,4 +1,4 @@ -gradlePluginsVersion=0.13.1 +gradlePluginsVersion=0.13.2 kotlinVersion=1.1.1 guavaVersion=21.0 bouncycastleVersion=1.57 diff --git a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy index 6d41b9d370..6eafcdf3cd 100644 --- a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy +++ b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy @@ -24,7 +24,11 @@ class Cordformation implements Plugin { // Note: project.afterEvaluate did not have full dependency resolution completed, hence a task is used instead def task = project.task('configureCordappFatJar') { doLast { - project.tasks.jar.from getDirectNonCordaDependencies(project).collect { project.zipTree(it) }.flatten() + project.tasks.jar.from(getDirectNonCordaDependencies(project).collect { project.zipTree(it)}) { + exclude "META-INF/*.SF" + exclude "META-INF/*.DSA" + exclude "META-INF/*.RSA" + } } } project.tasks.jar.dependsOn task @@ -43,7 +47,7 @@ class Cordformation implements Plugin { }, filePathInJar).asFile() } - static def getDirectNonCordaDependencies(Project project) { + private static def getDirectNonCordaDependencies(Project project) { def coreCordaNames = ['jfx', 'mock', 'rpc', 'core', 'corda', 'cordform-common', 'corda-webserver', 'finance', 'node', 'node-api', 'node-schemas', 'test-utils', 'jackson', 'verifier', 'webserver', 'capsule', 'webcapsule'] def excludes = coreCordaNames.collect { [group: 'net.corda', name: it] } + [ [group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib'], From d2869e4f456dbebfee150a243db79f4373cc39cf Mon Sep 17 00:00:00 2001 From: Andrzej Cichocki Date: Fri, 7 Jul 2017 15:50:50 +0100 Subject: [PATCH 77/97] Refactor then/success/failure (#984) to make ListenableFuture replacement less fiddly. --- .../rpc/ClientRPCInfrastructureTests.kt | 10 ++-- core/src/main/kotlin/net/corda/core/Utils.kt | 44 +++++------------- .../corda/core/concurrent/ConcurrencyUtils.kt | 4 +- .../kotlin/net/corda/node/internal/Node.kt | 46 +++++++++---------- .../net/corda/node/internal/NodeStartup.kt | 6 +-- .../services/messaging/NodeMessagingClient.kt | 4 +- .../statemachine/StateMachineManager.kt | 2 +- .../node/shell/FlowWatchPrintingSubscriber.kt | 2 +- .../net/corda/node/shell/InteractiveShell.kt | 2 +- .../corda/netmap/simulation/IRSSimulation.kt | 13 ++---- .../corda/demobench/views/NodeTerminalView.kt | 24 +++++----- .../net/corda/explorer/ExplorerSimulation.kt | 9 ++-- .../views/cordapps/cash/NewTransaction.kt | 7 ++- .../net/corda/loadtest/NodeConnection.kt | 4 +- .../net/corda/loadtest/tests/CrossCashTest.kt | 12 ++--- .../net/corda/loadtest/tests/NotaryTest.kt | 6 +-- .../net/corda/loadtest/tests/StabilityTest.kt | 12 ++--- 17 files changed, 90 insertions(+), 117 deletions(-) diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/ClientRPCInfrastructureTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/ClientRPCInfrastructureTests.kt index c9d3c65879..0117504c2e 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/ClientRPCInfrastructureTests.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/ClientRPCInfrastructureTests.kt @@ -5,7 +5,7 @@ import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.SettableFuture import net.corda.core.getOrThrow import net.corda.core.messaging.RPCOps -import net.corda.core.success +import net.corda.core.thenMatch import net.corda.node.services.messaging.getRpcContext import net.corda.nodeapi.RPCSinceVersion import net.corda.testing.RPCDriverExposedDSLInterface @@ -158,12 +158,12 @@ class ClientRPCInfrastructureTests : AbstractRPCTest() { val clientQuotes = LinkedBlockingQueue() val clientFuture = proxy.makeComplicatedListenableFuture() - clientFuture.success { + clientFuture.thenMatch({ val name = it.first - it.second.success { + it.second.thenMatch({ clientQuotes += "Quote by $name: $it" - } - } + }, {}) + }, {}) assertThat(clientQuotes).isEmpty() diff --git a/core/src/main/kotlin/net/corda/core/Utils.kt b/core/src/main/kotlin/net/corda/core/Utils.kt index 541240ae11..8bd165d370 100644 --- a/core/src/main/kotlin/net/corda/core/Utils.kt +++ b/core/src/main/kotlin/net/corda/core/Utils.kt @@ -25,7 +25,6 @@ import java.time.Duration import java.time.temporal.Temporal import java.util.concurrent.* import java.util.concurrent.locks.ReentrantLock -import java.util.function.BiConsumer import java.util.stream.Stream import java.util.zip.Deflater import java.util.zip.ZipEntry @@ -67,38 +66,20 @@ fun Future.getOrThrow(timeout: Duration? = null): T { } } -fun future(block: () -> T): ListenableFuture = CompletableToListenable(CompletableFuture.supplyAsync(block)) +fun future(block: () -> V): Future = CompletableFuture.supplyAsync(block) -private class CompletableToListenable(private val base: CompletableFuture) : Future by base, ListenableFuture { - override fun addListener(listener: Runnable, executor: Executor) { - base.whenCompleteAsync(BiConsumer { _, _ -> listener.run() }, executor) - } -} +fun , V> F.then(block: (F) -> V) = addListener(Runnable { block(this) }, MoreExecutors.directExecutor()) -// Some utilities for working with Guava listenable futures. -fun ListenableFuture.then(executor: Executor, body: () -> Unit) = addListener(Runnable(body), executor) - -fun ListenableFuture.success(executor: Executor, body: (T) -> Unit) = then(executor) { - val r = try { - get() - } catch(e: Throwable) { - return@then - } - body(r) -} - -fun ListenableFuture.failure(executor: Executor, body: (Throwable) -> Unit) = then(executor) { - try { +fun Future.match(success: (U) -> V, failure: (Throwable) -> V): V { + return success(try { getOrThrow() } catch (t: Throwable) { - body(t) - } + return failure(t) + }) } -infix fun ListenableFuture.then(body: () -> Unit): ListenableFuture = apply { then(RunOnCallerThread, body) } -infix fun ListenableFuture.success(body: (T) -> Unit): ListenableFuture = apply { success(RunOnCallerThread, body) } -infix fun ListenableFuture.failure(body: (Throwable) -> Unit): ListenableFuture = apply { failure(RunOnCallerThread, body) } -fun ListenableFuture<*>.andForget(log: Logger) = failure(RunOnCallerThread) { log.error("Background task failed:", it) } +fun ListenableFuture.thenMatch(success: (U) -> V, failure: (Throwable) -> W) = then { it.match(success, failure) } +fun ListenableFuture<*>.andForget(log: Logger) = then { it.match({}, { log.error("Background task failed:", it) }) } @Suppress("UNCHECKED_CAST") // We need the awkward cast because otherwise F cannot be nullable, even though it's safe. infix fun ListenableFuture.map(mapper: (F) -> T): ListenableFuture = Futures.transform(this, { (mapper as (F?) -> T)(it) }) infix fun ListenableFuture.flatMap(mapper: (F) -> ListenableFuture): ListenableFuture = Futures.transformAsync(this) { mapper(it!!) } @@ -114,12 +95,12 @@ inline fun SettableFuture.catch(block: () -> T) { fun ListenableFuture.toObservable(): Observable { return Observable.create { subscriber -> - success { + thenMatch({ subscriber.onNext(it) subscriber.onCompleted() - } failure { + }, { subscriber.onError(it) - } + }) } } @@ -204,9 +185,6 @@ fun List.randomOrNull(): T? { /** Returns a random element in the list matching the given predicate, or null if none found */ fun List.randomOrNull(predicate: (T) -> Boolean) = filter(predicate).randomOrNull() -// An alias that can sometimes make code clearer to read. -val RunOnCallerThread: Executor = MoreExecutors.directExecutor() - inline fun elapsedTime(block: () -> Unit): Duration { val start = System.nanoTime() block() diff --git a/core/src/main/kotlin/net/corda/core/concurrent/ConcurrencyUtils.kt b/core/src/main/kotlin/net/corda/core/concurrent/ConcurrencyUtils.kt index 11750fe3e8..8ab4d6f4e1 100644 --- a/core/src/main/kotlin/net/corda/core/concurrent/ConcurrencyUtils.kt +++ b/core/src/main/kotlin/net/corda/core/concurrent/ConcurrencyUtils.kt @@ -4,7 +4,7 @@ import com.google.common.annotations.VisibleForTesting import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.SettableFuture import net.corda.core.catch -import net.corda.core.failure +import net.corda.core.match import net.corda.core.then import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -29,7 +29,7 @@ internal fun firstOf(futures: Array>, log: Lo if (winnerChosen.compareAndSet(false, true)) { resultFuture.catch { handler(it) } } else if (!it.isCancelled) { - it.failure { log.error(shortCircuitedTaskFailedMessage, it) } + it.match({}, { log.error(shortCircuitedTaskFailedMessage, it) }) } } } diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index c9ec5fcac3..1e5c973df5 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -4,13 +4,11 @@ import com.codahale.metrics.JmxReporter import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.SettableFuture -import net.corda.core.flatMap +import net.corda.core.* import net.corda.core.messaging.RPCOps -import net.corda.core.minutes import net.corda.core.node.ServiceHub import net.corda.core.node.services.ServiceInfo import net.corda.core.seconds -import net.corda.core.success import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.loggerFor import net.corda.core.utilities.parseNetworkHostAndPort @@ -298,27 +296,29 @@ open class Node(override val configuration: FullNodeConfiguration, override fun start(): Node { super.start() - networkMapRegistrationFuture.success(serverThread) { - // Begin exporting our own metrics via JMX. These can be monitored using any agent, e.g. Jolokia: - // - // https://jolokia.org/agent/jvm.html - JmxReporter. - forRegistry(services.monitoringService.metrics). - inDomain("net.corda"). - createsObjectNamesWith { _, domain, name -> - // Make the JMX hierarchy a bit better organised. - val category = name.substringBefore('.') - val subName = name.substringAfter('.', "") - if (subName == "") - ObjectName("$domain:name=$category") - else - ObjectName("$domain:type=$category,name=$subName") - }. - build(). - start() + networkMapRegistrationFuture.thenMatch({ + serverThread.execute { + // Begin exporting our own metrics via JMX. These can be monitored using any agent, e.g. Jolokia: + // + // https://jolokia.org/agent/jvm.html + JmxReporter. + forRegistry(services.monitoringService.metrics). + inDomain("net.corda"). + createsObjectNamesWith { _, domain, name -> + // Make the JMX hierarchy a bit better organised. + val category = name.substringBefore('.') + val subName = name.substringAfter('.', "") + if (subName == "") + ObjectName("$domain:name=$category") + else + ObjectName("$domain:type=$category,name=$subName") + }. + build(). + start() - (startupComplete as SettableFuture).set(Unit) - } + (startupComplete as SettableFuture).set(Unit) + } + }, {}) shutdownHook = addShutdownHook { stop() } diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt index b9824e7c6d..f819f8fa31 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -103,7 +103,7 @@ open class NodeStartup(val args: Array) { node.start() printPluginsAndServices(node) - node.networkMapRegistrationFuture.success { + node.networkMapRegistrationFuture.thenMatch({ val elapsed = (System.currentTimeMillis() - startTime) / 10 / 100.0 // TODO: Replace this with a standard function to get an unambiguous rendering of the X.500 name. val name = node.info.legalIdentity.name.orgName ?: node.info.legalIdentity.name.commonName @@ -111,14 +111,14 @@ open class NodeStartup(val args: Array) { // Don't start the shell if there's no console attached. val runShell = !cmdlineOptions.noLocalShell && System.console() != null - node.startupComplete then { + node.startupComplete.then { try { InteractiveShell.startShell(cmdlineOptions.baseDirectory, runShell, cmdlineOptions.sshdServer, node) } catch(e: Throwable) { logger.error("Shell failed to start", e) } } - } + }, {}) node.run() } diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt index 713c1be649..0b9a45d387 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt @@ -184,7 +184,7 @@ class NodeMessagingClient(override val config: NodeConfiguration, // Create a queue, consumer and producer for handling P2P network messages. p2pConsumer = makeP2PConsumer(session, true) - networkMapRegistrationFuture.success { + networkMapRegistrationFuture.thenMatch({ state.locked { log.info("Network map is complete, so removing filter from P2P consumer.") try { @@ -194,7 +194,7 @@ class NodeMessagingClient(override val config: NodeConfiguration, } p2pConsumer = makeP2PConsumer(session, false) } - } + }, {}) rpcServer = RPCServer(rpcOps, NODE_USER, NODE_USER, locator, userService, config.myLegalName) diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt index fcae71e21b..53e8a460eb 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt @@ -195,7 +195,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, fun start() { restoreFibersFromCheckpoints() listenToLedgerTransactions() - serviceHub.networkMapCache.mapServiceRegistered.then(executor) { resumeRestoredFibers() } + serviceHub.networkMapCache.mapServiceRegistered.then { executor.execute(this::resumeRestoredFibers) } } private fun listenToLedgerTransactions() { diff --git a/node/src/main/kotlin/net/corda/node/shell/FlowWatchPrintingSubscriber.kt b/node/src/main/kotlin/net/corda/node/shell/FlowWatchPrintingSubscriber.kt index 0f11a6a547..fa6b477321 100644 --- a/node/src/main/kotlin/net/corda/node/shell/FlowWatchPrintingSubscriber.kt +++ b/node/src/main/kotlin/net/corda/node/shell/FlowWatchPrintingSubscriber.kt @@ -25,7 +25,7 @@ class FlowWatchPrintingSubscriber(private val toStream: RenderPrintWriter) : Sub init { // The future is public and can be completed by something else to indicate we don't wish to follow // anymore (e.g. the user pressing Ctrl-C). - future then { unsubscribe() } + future.then { unsubscribe() } } @Synchronized diff --git a/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt b/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt index 3290740576..553890b946 100644 --- a/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt +++ b/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt @@ -394,7 +394,7 @@ object InteractiveShell { init { // The future is public and can be completed by something else to indicate we don't wish to follow // anymore (e.g. the user pressing Ctrl-C). - future then { unsubscribe() } + future.then { unsubscribe() } } @Synchronized diff --git a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt index 59b0f2497d..c4148f403e 100644 --- a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt +++ b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt @@ -3,10 +3,7 @@ package net.corda.netmap.simulation import co.paralleluniverse.fibers.Suspendable import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.readValue -import com.google.common.util.concurrent.FutureCallback -import com.google.common.util.concurrent.Futures -import com.google.common.util.concurrent.ListenableFuture -import com.google.common.util.concurrent.SettableFuture +import com.google.common.util.concurrent.* import net.corda.core.* import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.UniqueIdentifier @@ -49,7 +46,7 @@ class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, laten val future = SettableFuture.create() om = JacksonSupport.createInMemoryMapper(InMemoryIdentityService((banks + regulators + networkMap).map { it.info.legalIdentityAndCert }, trustRoot = DUMMY_CA.certificate)) - startIRSDealBetween(0, 1).success { + startIRSDealBetween(0, 1).thenMatch({ // Next iteration is a pause. executeOnNextIteration.add {} executeOnNextIteration.add { @@ -67,16 +64,16 @@ class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, laten executeOnNextIteration.add { val f = doNextFixing(0, 1) if (f != null) { - Futures.addCallback(f, this, RunOnCallerThread) + Futures.addCallback(f, this, MoreExecutors.directExecutor()) } else { // All done! future.set(Unit) } } } - }, RunOnCallerThread) + }, MoreExecutors.directExecutor()) } - } + }, {}) return future } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTerminalView.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTerminalView.kt index 7a44b60604..d2d81031c6 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTerminalView.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTerminalView.kt @@ -18,8 +18,7 @@ import javafx.scene.layout.HBox import javafx.scene.layout.VBox import javafx.util.Duration import net.corda.core.crypto.commonName -import net.corda.core.failure -import net.corda.core.success +import net.corda.core.match import net.corda.core.then import net.corda.core.messaging.CordaRPCOps import net.corda.demobench.explorer.ExplorerController @@ -157,19 +156,20 @@ class NodeTerminalView : Fragment() { launchWebButton.graphic = ProgressIndicator() log.info("Starting web server for ${config.legalName}") - webServer.open(config) then { + webServer.open(config).then { Platform.runLater { launchWebButton.graphic = null } - } success { - log.info("Web server for ${config.legalName} started on $it") - Platform.runLater { - webURL = it - launchWebButton.text = "Reopen\nweb site" - app.hostServices.showDocument(it.toString()) - } - } failure { - launchWebButton.text = oldLabel + it.match({ + log.info("Web server for ${config.legalName} started on $it") + Platform.runLater { + webURL = it + launchWebButton.text = "Reopen\nweb site" + app.hostServices.showDocument(it.toString()) + } + }, { + launchWebButton.text = oldLabel + }) } } } diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt index bbd38278e4..1ea92e3f48 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt @@ -10,14 +10,13 @@ import net.corda.contracts.asset.Cash import net.corda.core.contracts.Amount import net.corda.core.contracts.GBP import net.corda.core.contracts.USD -import net.corda.core.failure import net.corda.core.identity.Party import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.FlowHandle import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceType +import net.corda.core.thenMatch import net.corda.core.utilities.OpaqueBytes -import net.corda.core.success import net.corda.flows.* import net.corda.node.services.startFlowPermission import net.corda.node.services.transactions.SimpleNotaryService @@ -133,11 +132,11 @@ class ExplorerSimulation(val options: OptionSet) { // Log to logger when flow finish. fun FlowHandle.log(seq: Int, name: String) { val out = "[$seq] $name $id :" - returnValue.success { (stx) -> + returnValue.thenMatch({ (stx) -> Main.log.info("$out ${stx.id} ${(stx.tx.outputs.first().data as Cash.State).amount}") - }.failure { + }, { Main.log.info("$out ${it.message}") - } + }) } for (i in 0..maxIterations) { diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt index c44a1b7d2e..087615728f 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt @@ -28,7 +28,6 @@ import net.corda.core.messaging.FlowHandle import net.corda.core.messaging.startFlow import net.corda.core.node.NodeInfo import net.corda.core.utilities.OpaqueBytes -import net.corda.core.then import net.corda.core.transactions.SignedTransaction import net.corda.explorer.formatters.PartyNameFormatter import net.corda.explorer.model.CashTransaction @@ -106,7 +105,11 @@ class NewTransaction : Fragment() { command.startFlow(rpcProxy.value!!) } runAsync { - handle.returnValue.then { dialog.dialogPane.isDisable = false }.getOrThrow() + try { + handle.returnValue.getOrThrow() + } finally { + dialog.dialogPane.isDisable = false + } }.ui { it -> val stx: SignedTransaction = it.stx val type = when (command) { diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/NodeConnection.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/NodeConnection.kt index cecca567b4..7376ce4413 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/NodeConnection.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/NodeConnection.kt @@ -1,6 +1,5 @@ package net.corda.loadtest -import com.google.common.util.concurrent.ListenableFuture import com.jcraft.jsch.ChannelExec import com.jcraft.jsch.Session import net.corda.client.rpc.CordaRPCClient @@ -14,6 +13,7 @@ import net.corda.nodeapi.internal.addShutdownHook import java.io.ByteArrayOutputStream import java.io.Closeable import java.io.OutputStream +import java.util.concurrent.Future /** * [NodeConnection] allows executing remote shell commands on the node as well as executing RPCs. @@ -85,7 +85,7 @@ class NodeConnection(val remoteNode: RemoteNode, private val jSchSession: Sessio return ShellCommandOutput(command, exitCode, stdoutStream.toString(), stderrStream.toString()) } - private fun runShellCommand(command: String, stdout: OutputStream, stderr: OutputStream): ListenableFuture { + private fun runShellCommand(command: String, stdout: OutputStream, stderr: OutputStream): Future { log.info("Running '$command' on ${remoteNode.hostname}") return future { val (exitCode, _) = withChannelExec(command) { channel -> diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/CrossCashTest.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/CrossCashTest.kt index d247b614f7..6753020d98 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/CrossCashTest.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/CrossCashTest.kt @@ -7,10 +7,9 @@ import net.corda.contracts.asset.Cash import net.corda.core.contracts.Issued import net.corda.core.contracts.PartyAndReference import net.corda.core.contracts.USD -import net.corda.core.failure import net.corda.core.identity.AbstractParty +import net.corda.core.thenMatch import net.corda.core.utilities.OpaqueBytes -import net.corda.core.success import net.corda.flows.CashFlowCommand import net.corda.loadtest.LoadTest import net.corda.loadtest.NodeConnection @@ -207,12 +206,11 @@ val crossCashTest = LoadTest( execute = { command -> val result = command.command.startFlow(command.node.proxy).returnValue - result.failure { - log.error("Failure[$command]", it) - } - result.success { + result.thenMatch({ log.info("Success[$command]: $result") - } + }, { + log.error("Failure[$command]", it) + }) }, gatherRemoteState = { previousState -> diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt index 9c3486538d..b9440ee675 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt @@ -9,7 +9,7 @@ import net.corda.contracts.asset.DUMMY_CASH_ISSUER_KEY import net.corda.testing.contracts.DummyContract import net.corda.core.flows.FlowException import net.corda.core.messaging.startFlow -import net.corda.core.success +import net.corda.core.thenMatch import net.corda.core.transactions.SignedTransaction import net.corda.flows.FinalityFlow import net.corda.loadtest.LoadTest @@ -42,9 +42,9 @@ val dummyNotarisationTest = LoadTest( try { val proxy = node.proxy val issueFlow = proxy.startFlow(::FinalityFlow, issueTx) - issueFlow.returnValue.success { + issueFlow.returnValue.thenMatch({ val moveFlow = proxy.startFlow(::FinalityFlow, moveTx) - } + }, {}) } catch (e: FlowException) { log.error("Failure", e) } diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/StabilityTest.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/StabilityTest.kt index e9508cf324..b24f0216b4 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/StabilityTest.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/StabilityTest.kt @@ -3,11 +3,10 @@ package net.corda.loadtest.tests import net.corda.client.mock.Generator import net.corda.core.contracts.Amount import net.corda.core.contracts.USD -import net.corda.core.failure import net.corda.core.flows.FlowException import net.corda.core.getOrThrow +import net.corda.core.thenMatch import net.corda.core.utilities.OpaqueBytes -import net.corda.core.success import net.corda.core.utilities.loggerFor import net.corda.flows.CashFlowCommand import net.corda.loadtest.LoadTest @@ -25,12 +24,11 @@ object StabilityTest { interpret = { _, _ -> }, execute = { command -> val result = command.command.startFlow(command.node.proxy).returnValue - result.failure { - log.error("Failure[$command]", it) - } - result.success { + result.thenMatch({ log.info("Success[$command]: $result") - } + }, { + log.error("Failure[$command]", it) + }) }, gatherRemoteState = {} ) From 365364ddd5c94b1db92a691f6f5aed294a4216cb Mon Sep 17 00:00:00 2001 From: Andrzej Cichocki Date: Mon, 10 Jul 2017 09:30:28 +0100 Subject: [PATCH 78/97] Re-enable BFT tests after porting away from NodeBasedTest (#889) --- .../kotlin/net/corda/node/services/BFTNotaryServiceTests.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt index 0f8a0c40e4..5f257894e4 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt @@ -67,13 +67,11 @@ class BFTNotaryServiceTests { } @Test - @Ignore("Under investigation due to failure on TC build server") fun `detect double spend 1 faulty`() { detectDoubleSpend(1) } @Test - @Ignore("Under investigation due to failure on TC build server") fun `detect double spend 2 faulty`() { detectDoubleSpend(2) } From f718acb939ad4a3a138d20cf5f9352e110c9d5c8 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Fri, 7 Jul 2017 12:49:54 +0100 Subject: [PATCH 79/97] Moved CordaException.kt to base core package --- .../kotlin/net/corda/core/{utilities => }/CordaException.kt | 2 +- core/src/main/kotlin/net/corda/core/flows/FlowException.kt | 4 ++-- .../core/serialization/amqp/custom/ThrowableSerializer.kt | 4 ++-- .../corda/core/serialization/amqp/SerializationOutputTests.kt | 2 +- node-api/src/main/kotlin/net/corda/nodeapi/RPCStructures.kt | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) rename core/src/main/kotlin/net/corda/core/{utilities => }/CordaException.kt (99%) diff --git a/core/src/main/kotlin/net/corda/core/utilities/CordaException.kt b/core/src/main/kotlin/net/corda/core/CordaException.kt similarity index 99% rename from core/src/main/kotlin/net/corda/core/utilities/CordaException.kt rename to core/src/main/kotlin/net/corda/core/CordaException.kt index 907bbee408..49ed6b6975 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/CordaException.kt +++ b/core/src/main/kotlin/net/corda/core/CordaException.kt @@ -1,4 +1,4 @@ -package net.corda.core.utilities +package net.corda.core import net.corda.core.serialization.CordaSerializable import java.util.* diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowException.kt b/core/src/main/kotlin/net/corda/core/flows/FlowException.kt index 153baeae46..e527f22c55 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowException.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowException.kt @@ -1,7 +1,7 @@ package net.corda.core.flows -import net.corda.core.utilities.CordaException -import net.corda.core.utilities.CordaRuntimeException +import net.corda.core.CordaException +import net.corda.core.CordaRuntimeException // DOCSTART 1 /** 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 index ed267ed44d..7196667a41 100644 --- 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 @@ -4,8 +4,8 @@ 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 net.corda.core.CordaRuntimeException +import net.corda.core.CordaThrowable import java.io.NotSerializableException class ThrowableSerializer(factory: SerializerFactory) : CustomSerializer.Proxy(Throwable::class.java, ThrowableProxy::class.java, factory) { diff --git a/core/src/test/kotlin/net/corda/core/serialization/amqp/SerializationOutputTests.kt b/core/src/test/kotlin/net/corda/core/serialization/amqp/SerializationOutputTests.kt index 52e7ca29a8..54771ac805 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/amqp/SerializationOutputTests.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/amqp/SerializationOutputTests.kt @@ -7,7 +7,7 @@ import net.corda.core.identity.AbstractParty import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.EmptyWhitelist import net.corda.core.serialization.KryoAMQPSerializer -import net.corda.core.utilities.CordaRuntimeException +import net.corda.core.CordaRuntimeException import net.corda.nodeapi.RPCException import net.corda.testing.MEGA_CORP import net.corda.testing.MEGA_CORP_PUBKEY diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/RPCStructures.kt b/node-api/src/main/kotlin/net/corda/nodeapi/RPCStructures.kt index e5f279c692..0b5236f68f 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/RPCStructures.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/RPCStructures.kt @@ -9,7 +9,7 @@ import net.corda.core.requireExternal import net.corda.core.serialization.* import net.corda.core.toFuture import net.corda.core.toObservable -import net.corda.core.utilities.CordaRuntimeException +import net.corda.core.CordaRuntimeException import net.corda.nodeapi.config.OldConfig import rx.Observable import java.io.InputStream From fd3a8274386fe6999086baf9d6fc080119e54c98 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Mon, 10 Jul 2017 11:50:24 +0100 Subject: [PATCH 80/97] Annotation testing --- .../serialization/carpenter/ClassCarpenter.kt | 11 ++++---- .../carpenter/ClassCarpenterTest.kt | 27 +++++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/serialization/carpenter/ClassCarpenter.kt b/core/src/main/kotlin/net/corda/core/serialization/carpenter/ClassCarpenter.kt index 784cc5d0f5..2544727a9c 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/carpenter/ClassCarpenter.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/carpenter/ClassCarpenter.kt @@ -90,20 +90,21 @@ class ClassCarpenter { get() = if (this.field.isPrimitive) this.descriptor else "Ljava/lang/Object;" fun generateField(cw: ClassWriter) { + println ("generateField $name $nullabilityAnnotation") val fieldVisitor = cw.visitField(ACC_PROTECTED + ACC_FINAL, name, descriptor, null, null) - cw.visitAnnotation(nullabilityAnnotation, false).visitEnd() + fieldVisitor.visitAnnotation(nullabilityAnnotation, true).visitEnd() fieldVisitor.visitEnd() } fun addNullabilityAnnotation(mv: MethodVisitor) { - mv.visitAnnotation(nullabilityAnnotation, false) + mv.visitAnnotation(nullabilityAnnotation, true).visitEnd() } fun visitParameter(mv: MethodVisitor, idx: Int) { with(mv) { visitParameter(name, 0) if (!field.isPrimitive) { - visitParameterAnnotation(idx, nullabilityAnnotation, false).visitEnd() + visitParameterAnnotation(idx, nullabilityAnnotation, true).visitEnd() } } } @@ -113,7 +114,7 @@ class ClassCarpenter { } class NonNullableField(field: Class) : Field(field) { - override val nullabilityAnnotation = "Ljavax/annotations/NotNull;" + override val nullabilityAnnotation = "Ljavax/annotation/Nonnull;" constructor(name: String, field: Class) : this(field) { this.name = name @@ -141,7 +142,7 @@ class ClassCarpenter { class NullableField(field: Class) : Field(field) { - override val nullabilityAnnotation = "Ljavax/annotations/Nullable;" + override val nullabilityAnnotation = "Ljavax/annotation/Nullable;" constructor(name: String, field: Class) : this(field) { if (field.isPrimitive) { diff --git a/core/src/test/kotlin/net/corda/core/serialization/carpenter/ClassCarpenterTest.kt b/core/src/test/kotlin/net/corda/core/serialization/carpenter/ClassCarpenterTest.kt index ef9d75f640..6dc4f5b12a 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/carpenter/ClassCarpenterTest.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/carpenter/ClassCarpenterTest.kt @@ -5,6 +5,7 @@ import org.junit.Test import java.lang.reflect.Field import java.lang.reflect.Method import kotlin.test.assertEquals +import kotlin.test.assertTrue class ClassCarpenterTest { @@ -464,4 +465,30 @@ class ClassCarpenterTest { assertEquals("some pickles", arr2[0]) assertEquals("some fries", arr2[1]) } + + @Test + fun `nullable sets annotations`() { + val className = "iEnjoyJam" + val schema = ClassCarpenter.ClassSchema( + "gen.$className", + mapOf("a" to ClassCarpenter.NullableField(String::class.java), + "b" to ClassCarpenter.NonNullableField(String::class.java))) + + val clazz = cc.build(schema) + + assertEquals (2, clazz.declaredFields.size) + + assertEquals (1, clazz.getDeclaredField("a").annotations.size) + assertEquals (javax.annotation.Nullable::class.java, clazz.getDeclaredField("a").annotations[0].annotationClass.java) + + assertEquals (1, clazz.getDeclaredField("b").annotations.size) + assertEquals (javax.annotation.Nonnull::class.java, clazz.getDeclaredField("b").annotations[0].annotationClass.java) + + assertEquals (1, clazz.getMethod("getA").annotations.size) + assertEquals (javax.annotation.Nullable::class.java, clazz.getMethod("getA").annotations[0].annotationClass.java) + + assertEquals (1, clazz.getMethod("getB").annotations.size) + assertEquals (javax.annotation.Nonnull::class.java, clazz.getMethod("getB").annotations[0].annotationClass.java) + } + } From c81ef7eb93d80cc3381a8077bee1d0cc6eb9ca10 Mon Sep 17 00:00:00 2001 From: josecoll Date: Mon, 10 Jul 2017 12:49:00 +0100 Subject: [PATCH 81/97] Vault Query Sort by StateRef (or constituents: txId, index) (#990) * Provide sorting by state reference (and individual constituents of: txId, index) * Fixed formatting. * Updated import following rebase from master. * Updated import following rebase from master. --- .../node/services/vault/QueryCriteriaUtils.kt | 12 +++- docs/source/changelog.rst | 3 + .../vault/HibernateQueryCriteriaParser.kt | 51 ++++++++------ .../services/vault/VaultQueryJavaTests.java | 26 +++++++ .../database/HibernateConfigurationTest.kt | 61 +++++++++++++++++ .../node/services/vault/VaultQueryTests.kt | 68 +++++++++++++++++-- .../corda/testing/contracts/VaultFiller.kt | 8 ++- 7 files changed, 200 insertions(+), 29 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt index 8f3a528b77..6c72d34672 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt @@ -150,7 +150,13 @@ data class Sort(val columns: Collection) { @CordaSerializable interface Attribute - enum class VaultStateAttribute(val columnName: String) : Attribute { + enum class CommonStateAttribute(val attributeParent: String, val attributeChild: String?) : Attribute { + STATE_REF("stateRef", null), + STATE_REF_TXN_ID("stateRef", "txId"), + STATE_REF_INDEX("stateRef", "index") + } + + enum class VaultStateAttribute(val attributeName: String) : Attribute { /** Vault States */ NOTARY_NAME("notaryName"), CONTRACT_TYPE("contractStateClassName"), @@ -160,14 +166,14 @@ data class Sort(val columns: Collection) { LOCK_ID("lockId") } - enum class LinearStateAttribute(val columnName: String) : Attribute { + enum class LinearStateAttribute(val attributeName: String) : Attribute { /** Vault Linear States */ UUID("uuid"), EXTERNAL_ID("externalId"), DEAL_REFERENCE("dealReference") } - enum class FungibleStateAttribute(val columnName: String) : Attribute { + enum class FungibleStateAttribute(val attributeName: String) : Attribute { /** Vault Fungible States */ QUANTITY("quantity"), ISSUER_REF("issuerRef") diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index fa674479d4..7e4eb64739 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -28,6 +28,9 @@ UNRELEASED * ``random63BitValue()`` has moved to ``CryptoUtils`` +* Added additional common Sort attributes (see ``Sort.CommandStateAttribute``) for use in Vault Query criteria + to include STATE_REF, STATE_REF_TXN_ID, STATE_REF_INDEX + Milestone 13 ------------ diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt index eef63d1d68..8a55057ac3 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt @@ -334,7 +334,7 @@ class HibernateQueryCriteriaParser(val contractType: Class, val leftPredicates = parse(left) val rightPredicates = parse(right) - val andPredicate = criteriaBuilder.and(criteriaBuilder.and(*leftPredicates.toTypedArray(), *rightPredicates.toTypedArray())) + val andPredicate = criteriaBuilder.and(*leftPredicates.toTypedArray(), *rightPredicates.toTypedArray()) predicateSet.add(andPredicate) return predicateSet @@ -378,11 +378,11 @@ class HibernateQueryCriteriaParser(val contractType: Class, var orderCriteria = mutableListOf() sorting.columns.map { (sortAttribute, direction) -> - val (entityStateClass, entityStateColumnName) = - when(sortAttribute) { - is SortAttribute.Standard -> parse(sortAttribute.attribute) - is SortAttribute.Custom -> Pair(sortAttribute.entityStateClass, sortAttribute.entityStateColumnName) - } + val (entityStateClass, entityStateAttributeParent, entityStateAttributeChild) = + when(sortAttribute) { + is SortAttribute.Standard -> parse(sortAttribute.attribute) + is SortAttribute.Custom -> Triple(sortAttribute.entityStateClass, sortAttribute.entityStateColumnName, null) + } val sortEntityRoot = rootEntities.getOrElse(entityStateClass) { // scenario where sorting on attributes not parsed as criteria @@ -394,10 +394,16 @@ class HibernateQueryCriteriaParser(val contractType: Class, } when (direction) { Sort.Direction.ASC -> { - orderCriteria.add(criteriaBuilder.asc(sortEntityRoot.get(entityStateColumnName))) + if (entityStateAttributeChild != null) + orderCriteria.add(criteriaBuilder.asc(sortEntityRoot.get(entityStateAttributeParent).get(entityStateAttributeChild))) + else + orderCriteria.add(criteriaBuilder.asc(sortEntityRoot.get(entityStateAttributeParent))) } Sort.Direction.DESC -> - orderCriteria.add(criteriaBuilder.desc(sortEntityRoot.get(entityStateColumnName))) + if (entityStateAttributeChild != null) + orderCriteria.add(criteriaBuilder.desc(sortEntityRoot.get(entityStateAttributeParent).get(entityStateAttributeChild))) + else + orderCriteria.add(criteriaBuilder.desc(sortEntityRoot.get(entityStateAttributeParent))) } } if (orderCriteria.isNotEmpty()) { @@ -406,20 +412,23 @@ class HibernateQueryCriteriaParser(val contractType: Class, } } - private fun parse(sortAttribute: Sort.Attribute): Pair, String> { - val entityClassAndColumnName : Pair, String> = - when(sortAttribute) { - is Sort.VaultStateAttribute -> { - Pair(VaultSchemaV1.VaultStates::class.java, sortAttribute.columnName) + private fun parse(sortAttribute: Sort.Attribute): Triple, String, String?> { + val entityClassAndColumnName : Triple, String, String?> = + when(sortAttribute) { + is Sort.CommonStateAttribute -> { + Triple(VaultSchemaV1.VaultStates::class.java, sortAttribute.attributeParent, sortAttribute.attributeChild) + } + is Sort.VaultStateAttribute -> { + Triple(VaultSchemaV1.VaultStates::class.java, sortAttribute.attributeName, null) + } + is Sort.LinearStateAttribute -> { + Triple(VaultSchemaV1.VaultLinearStates::class.java, sortAttribute.attributeName, null) + } + is Sort.FungibleStateAttribute -> { + Triple(VaultSchemaV1.VaultFungibleStates::class.java, sortAttribute.attributeName, null) + } + else -> throw VaultQueryException("Invalid sort attribute: $sortAttribute") } - is Sort.LinearStateAttribute -> { - Pair(VaultSchemaV1.VaultLinearStates::class.java, sortAttribute.columnName) - } - is Sort.FungibleStateAttribute -> { - Pair(VaultSchemaV1.VaultFungibleStates::class.java, sortAttribute.columnName) - } - else -> throw VaultQueryException("Invalid sort attribute: $sortAttribute") - } return entityClassAndColumnName } } \ No newline at end of file diff --git a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java index d88d023b5c..c1f58249e1 100644 --- a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java +++ b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java @@ -55,6 +55,7 @@ import static net.corda.node.utilities.DatabaseSupportKt.transaction; import static net.corda.testing.CoreTestUtils.getMEGA_CORP; import static net.corda.testing.CoreTestUtils.getMEGA_CORP_KEY; import static net.corda.testing.node.MockServicesKt.makeTestDataSourceProperties; +import static net.corda.core.utilities.ByteArrays.toHexString; import static org.assertj.core.api.Assertions.assertThat; public class VaultQueryJavaTests { @@ -134,6 +135,31 @@ public class VaultQueryJavaTests { }); } + @Test + public void unconsumedStatesForStateRefsSortedByTxnId() { + transaction(database, tx -> { + + VaultFiller.fillWithSomeTestLinearStates(services, 8); + Vault issuedStates = VaultFiller.fillWithSomeTestLinearStates(services, 2); + + Stream stateRefsStream = StreamSupport.stream(issuedStates.getStates().spliterator(), false).map(StateAndRef::getRef); + List stateRefs = stateRefsStream.collect(Collectors.toList()); + + SortAttribute.Standard sortAttribute = new SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_TXN_ID); + Sort sorting = new Sort(Arrays.asList(new Sort.SortColumn(sortAttribute, Sort.Direction.ASC))); + VaultQueryCriteria criteria = new VaultQueryCriteria(Vault.StateStatus.UNCONSUMED, null, stateRefs); + Vault.Page results = vaultQuerySvc.queryBy(DummyLinearContract.State.class, criteria, sorting); + + assertThat(results.getStates()).hasSize(2); + + stateRefs.sort(Comparator.comparing(stateRef -> toHexString(stateRef.getTxhash().getBytes()))); + assertThat(results.getStates().get(0).getRef()).isEqualTo(stateRefs.get(0)); + assertThat(results.getStates().get(1).getRef()).isEqualTo(stateRefs.get(1)); + + return tx; + }); + } + @Test public void consumedCashStates() { transaction(database, tx -> { diff --git a/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt index b7f023d8af..9081d6e72f 100644 --- a/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt @@ -202,6 +202,67 @@ class HibernateConfigurationTest { queryResultsAsc.map { println(it.recordedTime) } } + @Test + fun `with sorting by state ref desc and asc`() { + // generate additional state ref indexes + database.transaction { + services.consumeCash(1.DOLLARS) + services.consumeCash(2.DOLLARS) + services.consumeCash(3.DOLLARS) + services.consumeCash(4.DOLLARS) + services.consumeCash(5.DOLLARS) + } + + // structure query + val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java) + val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) + + val sortByStateRef = vaultStates.get("stateRef") + + // order by DESC + criteriaQuery.orderBy(criteriaBuilder.desc(sortByStateRef)) + val queryResults = entityManager.createQuery(criteriaQuery).resultList + println("DESC by stateRef") + queryResults.map { println(it.stateRef) } + + // order by ASC + criteriaQuery.orderBy(criteriaBuilder.asc(sortByStateRef)) + val queryResultsAsc = entityManager.createQuery(criteriaQuery).resultList + println("ASC by stateRef") + queryResultsAsc.map { println(it.stateRef) } + } + + @Test + fun `with sorting by state ref index and txId desc and asc`() { + // generate additional state ref indexes + database.transaction { + services.consumeCash(1.DOLLARS) + services.consumeCash(2.DOLLARS) + services.consumeCash(3.DOLLARS) + services.consumeCash(4.DOLLARS) + services.consumeCash(5.DOLLARS) + } + + // structure query + val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java) + val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) + + val sortByIndex = vaultStates.get("stateRef").get("index") + val sortByTxId = vaultStates.get("stateRef").get("txId") + + // order by DESC + criteriaQuery.orderBy(criteriaBuilder.desc(sortByIndex), criteriaBuilder.desc(sortByTxId)) + val queryResults = entityManager.createQuery(criteriaQuery).resultList + println("DESC by index txId") + queryResults.map { println(it.stateRef) } + + // order by ASC + criteriaQuery.orderBy(criteriaBuilder.asc(sortByIndex), criteriaBuilder.asc(sortByTxId)) + val queryResultsAsc = entityManager.createQuery(criteriaQuery).resultList + println("ASC by index txId") + queryResultsAsc.map { println(it.stateRef) } + } + @Test fun `with pagination`() { // add 100 additional cash entries diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index 0ff47da943..3e27f42042 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -18,6 +18,7 @@ import net.corda.core.schemas.testing.DummyLinearStateSchemaV1 import net.corda.core.seconds import net.corda.core.utilities.OpaqueBytes import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.toHexString import net.corda.node.services.database.HibernateConfiguration import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.vault.schemas.jpa.VaultSchemaV1 @@ -220,21 +221,80 @@ class VaultQueryTests { } @Test - fun `unconsumed states for state refs`() { + fun `unconsumed cash states sorted by state ref`() { + database.transaction { + var stateRefs : MutableList = mutableListOf() + + val issuedStates = services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L)) + val issuedStateRefs = issuedStates.states.map { it.ref }.toList() + stateRefs.addAll(issuedStateRefs) + + val spentStates = services.consumeCash(25.DOLLARS) + var spentStateRefs = spentStates.states.map { it.ref }.toList() + stateRefs.addAll(spentStateRefs) + + val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF) + val criteria = VaultQueryCriteria() + val results = vaultQuerySvc.queryBy(criteria, Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC)))) + + // default StateRef sort is by index then txnId: + // order by + // vaultschem1_.output_index, + // vaultschem1_.transaction_id asc + assertThat(results.states).hasSize(8) // -3 CONSUMED + 1 NEW UNCONSUMED (change) + + val sortedStateRefs = stateRefs.sortedBy { it.index } + + assertThat(results.states.first().ref.index).isEqualTo(sortedStateRefs.first().index) // 0 + assertThat(results.states.last().ref.index).isEqualTo(sortedStateRefs.last().index) // 1 + } + } + + @Test + fun `unconsumed cash states sorted by state ref txnId and index`() { + database.transaction { + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L)) + services.consumeCash(10.DOLLARS) + services.consumeCash(10.DOLLARS) + + val sortAttributeTxnId = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_TXN_ID) + val sortAttributeIndex = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_INDEX) + val sortBy = Sort(setOf(Sort.SortColumn(sortAttributeTxnId, Sort.Direction.ASC), + Sort.SortColumn(sortAttributeIndex, Sort.Direction.ASC))) + val criteria = VaultQueryCriteria() + val results = vaultQuerySvc.queryBy(criteria, sortBy) + + results.statesMetadata.forEach { + println(" ${it.ref}") + } + + // explicit sort order asc by txnId and then index: + // order by + // vaultschem1_.transaction_id asc, + // vaultschem1_.output_index asc + assertThat(results.states).hasSize(9) // -2 CONSUMED + 1 NEW UNCONSUMED (change) + } + } + + @Test + fun `unconsumed states for state refs`() { database.transaction { services.fillWithSomeTestLinearStates(8) val issuedStates = services.fillWithSomeTestLinearStates(2) val stateRefs = issuedStates.states.map { it.ref }.toList() // DOCSTART VaultQueryExample2 + val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_TXN_ID) val criteria = VaultQueryCriteria(stateRefs = listOf(stateRefs.first(), stateRefs.last())) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultQuerySvc.queryBy(criteria, Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC)))) // DOCEND VaultQueryExample2 assertThat(results.states).hasSize(2) - assertThat(results.states.first().ref).isEqualTo(issuedStates.states.first().ref) - assertThat(results.states.last().ref).isEqualTo(issuedStates.states.last().ref) + + val sortedStateRefs = stateRefs.sortedBy { it.txhash.bytes.toHexString() } + assertThat(results.states.first().ref).isEqualTo(sortedStateRefs.first()) + assertThat(results.states.last().ref).isEqualTo(sortedStateRefs.last()) } } diff --git a/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt b/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt index 0c39d2746e..d7ae9ff523 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt @@ -223,7 +223,7 @@ fun ServiceHub.evolveLinearStates(linearStates: List>) fun ServiceHub.evolveLinearState(linearState: StateAndRef) : StateAndRef = consumeAndProduce(linearState) @JvmOverloads -fun ServiceHub.consumeCash(amount: Amount, to: Party = CHARLIE) { +fun ServiceHub.consumeCash(amount: Amount, to: Party = CHARLIE): Vault { // A tx that spends our money. val spendTX = TransactionType.General.Builder(DUMMY_NOTARY).apply { vaultService.generateSpend(this, amount, to) @@ -231,4 +231,10 @@ fun ServiceHub.consumeCash(amount: Amount, to: Party = CHARLIE) { }.toSignedTransaction(checkSufficientSignatures = false) recordTransactions(spendTX) + + // Get all the StateRefs of all the generated transactions. + val states = spendTX.tx.outputs.indices.map { i -> spendTX.tx.outRef(i) } + + return Vault(states) } + From d52b0e5db978598198f0b5e77bf85b801c32a682 Mon Sep 17 00:00:00 2001 From: Konstantinos Chalkias Date: Tue, 11 Jul 2017 10:18:22 +0100 Subject: [PATCH 82/97] NodeAndWeight deterministic comparator (#1003) NodeAndWeight deterministic ordering by comparing hashes of public keys when required. --- .../net/corda/core/crypto/CompositeKey.kt | 9 +++++---- .../net/corda/core/crypto/EncodingUtils.kt | 2 +- .../corda/core/crypto/CompositeKeyTests.kt | 20 ++++++++++++++++++- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt b/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt index d7d3e8f205..75bdb2e73b 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt @@ -4,6 +4,7 @@ import net.corda.core.crypto.CompositeKey.NodeAndWeight import net.corda.core.serialization.CordaSerializable import org.bouncycastle.asn1.* import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo +import java.nio.ByteBuffer import java.security.PublicKey import java.util.* @@ -109,11 +110,11 @@ class CompositeKey private constructor (val threshold: Int, // We don't allow zero or negative weights. Minimum weight = 1. require (weight > 0) { "A non-positive weight was detected. Node info: $this" } } + override fun compareTo(other: NodeAndWeight): Int { - if (weight == other.weight) { - return node.hashCode().compareTo(other.node.hashCode()) - } - else return weight.compareTo(other.weight) + return if (weight == other.weight) { + ByteBuffer.wrap(node.toSHA256Bytes()).compareTo(ByteBuffer.wrap(other.node.toSHA256Bytes())) + } else weight.compareTo(other.weight) } override fun toASN1Primitive(): ASN1Primitive { diff --git a/core/src/main/kotlin/net/corda/core/crypto/EncodingUtils.kt b/core/src/main/kotlin/net/corda/core/crypto/EncodingUtils.kt index b1681a19f9..a79821b760 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/EncodingUtils.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/EncodingUtils.kt @@ -65,4 +65,4 @@ fun String.hexToBase64(): String = hexToByteArray().toBase64() // structure, e.g. mapping a PublicKey to a condition with the specific feature (ED25519). fun parsePublicKeyBase58(base58String: String): PublicKey = base58String.base58ToByteArray().deserialize() fun PublicKey.toBase58String(): String = this.serialize().bytes.toBase58() -fun PublicKey.toSHA256Bytes(): ByteArray = this.serialize().bytes.sha256().bytes +fun PublicKey.toSHA256Bytes(): ByteArray = this.serialize().bytes.sha256().bytes // TODO: decide on the format of hashed key (encoded Vs serialised). diff --git a/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt b/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt index 591d56001a..bee35e0a9d 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt @@ -1,7 +1,7 @@ package net.corda.core.crypto -import net.corda.core.utilities.OpaqueBytes import net.corda.core.serialization.serialize +import net.corda.core.utilities.OpaqueBytes import org.junit.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -259,4 +259,22 @@ class CompositeKeyTests { val signaturesWithoutRSA = listOf(K1Signature, R1Signature, EdSignature, SPSignature) assertFalse { compositeKey.isFulfilledBy(signaturesWithoutRSA.byKeys()) } } + + @Test + fun `CompositeKey deterministic children sorting`() { + val (_, pub1) = Crypto.generateKeyPair(Crypto.EDDSA_ED25519_SHA512) + val (_, pub2) = Crypto.generateKeyPair(Crypto.ECDSA_SECP256K1_SHA256) + val (_, pub3) = Crypto.generateKeyPair(Crypto.RSA_SHA256) + val (_, pub4) = Crypto.generateKeyPair(Crypto.EDDSA_ED25519_SHA512) + val (_, pub5) = Crypto.generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256) + val (_, pub6) = Crypto.generateKeyPair(Crypto.SPHINCS256_SHA256) + val (_, pub7) = Crypto.generateKeyPair(Crypto.ECDSA_SECP256K1_SHA256) + + // Using default weight = 1, thus all weights are equal. + val composite1 = CompositeKey.Builder().addKeys(pub1, pub2, pub3, pub4, pub5, pub6, pub7).build() as CompositeKey + // Store in reverse order. + val composite2 = CompositeKey.Builder().addKeys(pub7, pub6, pub5, pub4, pub3, pub2, pub1).build() as CompositeKey + // There are 7! = 5040 permutations, but as sorting is deterministic the following should never fail. + assertEquals(composite1.children, composite2.children) + } } From 7e8de79848a92a6690d6678b2174516a5e51a4dd Mon Sep 17 00:00:00 2001 From: Patrick Kuo Date: Tue, 11 Jul 2017 12:09:30 +0100 Subject: [PATCH 83/97] Legal name validation for X500Name (#983) * * Legal name validation for X500Name while loading from config file. * * Removed unintended changes. --- .../net/corda/core/utilities/LegalNameValidator.kt | 14 ++++++++++---- .../net/corda/nodeapi/config/ConfigUtilities.kt | 3 ++- .../net/corda/nodeapi/config/ConfigParsingTest.kt | 2 +- .../corda/services/messaging/P2PMessagingTest.kt | 2 +- .../corda/services/messaging/P2PSecurityTest.kt | 2 +- 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/utilities/LegalNameValidator.kt b/core/src/main/kotlin/net/corda/core/utilities/LegalNameValidator.kt index 804dd4ed7a..9ade89dfaf 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/LegalNameValidator.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/LegalNameValidator.kt @@ -2,6 +2,8 @@ package net.corda.core.utilities +import net.corda.core.crypto.commonName +import org.bouncycastle.asn1.x500.X500Name import java.lang.Character.UnicodeScript.* import java.text.Normalizer import java.util.regex.Pattern @@ -19,9 +21,13 @@ import javax.security.auth.x500.X500Principal * * @throws IllegalArgumentException if the name does not meet the required rules. The message indicates why not. */ -@Throws(IllegalArgumentException::class) fun validateLegalName(normalizedLegalName: String) { - rules.forEach { it.validate(normalizedLegalName) } + legalNameRules.forEach { it.validate(normalizedLegalName) } +} + +// TODO: Implement X500 attribute validation once the specification has been finalised. +fun validateX500Name(x500Name: X500Name) { + validateLegalName(x500Name.commonName) } val WHITESPACE = "\\s++".toRegex() @@ -35,7 +41,7 @@ fun normaliseLegalName(legalName: String): String { return Normalizer.normalize(trimmedLegalName, Normalizer.Form.NFKC) } -private val rules: List> = listOf( +private val legalNameRules: List> = listOf( UnicodeNormalizationRule(), CharacterRule(',', '=', '$', '"', '\'', '\\'), WordRule("node", "server"), @@ -107,7 +113,7 @@ private class X500NameRule : Rule { private class MustHaveAtLeastTwoLettersRule : Rule { override fun validate(legalName: String) { // Try to exclude names like "/", "£", "X" etc. - require(legalName.count { it.isLetter() } >= 3) { "Must have at least two letters" } + require(legalName.count { it.isLetter() } >= 3) { "Illegal input legal name '$legalName'. Legal name must have at least two letters" } } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/config/ConfigUtilities.kt b/node-api/src/main/kotlin/net/corda/nodeapi/config/ConfigUtilities.kt index f7cf998d7e..81455d6e6a 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/config/ConfigUtilities.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/config/ConfigUtilities.kt @@ -3,6 +3,7 @@ package net.corda.nodeapi.config import com.typesafe.config.Config import com.typesafe.config.ConfigUtil import net.corda.core.noneOrSingle +import net.corda.core.utilities.validateX500Name import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.parseNetworkHostAndPort import org.bouncycastle.asn1.x500.X500Name @@ -72,7 +73,7 @@ private fun Config.getSingleValue(path: String, type: KType): Any? { Path::class -> Paths.get(getString(path)) URL::class -> URL(getString(path)) Properties::class -> getConfig(path).toProperties() - X500Name::class -> X500Name(getString(path)) + X500Name::class -> X500Name(getString(path)).apply(::validateX500Name) else -> if (typeClass.java.isEnum) { parseEnum(typeClass.java, getString(path)) } else { diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/config/ConfigParsingTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/config/ConfigParsingTest.kt index 511eee6527..a3abc82492 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/config/ConfigParsingTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/config/ConfigParsingTest.kt @@ -112,7 +112,7 @@ class ConfigParsingTest { @Test fun x500Name() { - testPropertyType(getTestX509Name("Mock Node"), getTestX509Name("Mock Node 2"), valuesToString = true) + testPropertyType(getTestX509Name("Mock Party"), getTestX509Name("Mock Party 2"), valuesToString = true) } @Test diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt index d9eb27dbc9..d7e0dc2c3e 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt @@ -30,7 +30,7 @@ import java.util.concurrent.atomic.AtomicInteger class P2PMessagingTest : NodeBasedTest() { private companion object { val DISTRIBUTED_SERVICE_NAME = getTestX509Name("DistributedService") - val SERVICE_2_NAME = getTestX509Name("Service Node 2") + val SERVICE_2_NAME = getTestX509Name("Service 2") } @Test diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt index da327f6417..5036db9077 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt @@ -30,7 +30,7 @@ class P2PSecurityTest : NodeBasedTest() { @Test fun `incorrect legal name for the network map service config`() { - val incorrectNetworkMapName = X509Utilities.getDevX509Name(random63BitValue().toString()) + val incorrectNetworkMapName = X509Utilities.getDevX509Name("NetworkMap-${random63BitValue()}") val node = startNode(BOB.name, configOverrides = mapOf( "networkMapService" to mapOf( "address" to networkMapNode.configuration.p2pAddress.toString(), From 7caee508ec42848e800c36a931cbf7fee2369241 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Mon, 10 Jul 2017 12:16:00 +0100 Subject: [PATCH 84/97] Refactored ErrorOr into Try, with Success and Failure data sub-classes, and moved it into core.utilities --- .../kotlin/net/corda/client/mock/Generator.kt | 64 ++++++++-------- .../net/corda/client/rpc/RPCStabilityTests.kt | 17 +++-- .../rpc/internal/RPCClientProxyHandler.kt | 19 ++--- core/src/main/kotlin/net/corda/core/Utils.kt | 62 +--------------- .../net/corda/core/messaging/CordaRPCOps.kt | 4 +- .../kotlin/net/corda/core/utilities/Try.kt | 74 +++++++++++++++++++ .../main/kotlin/net/corda/nodeapi/RPCApi.kt | 5 +- .../node/services/BFTNotaryServiceTests.kt | 15 ++-- .../node/services/messaging/RPCServer.kt | 31 +++----- .../statemachine/FlowStateMachineImpl.kt | 14 ++-- .../statemachine/StateMachineManager.kt | 17 +++-- .../node/shell/FlowWatchPrintingSubscriber.kt | 25 ++++--- .../net/corda/node/utilities/AddOrRemove.kt | 1 - .../kotlin/net/corda/testing/driver/Driver.kt | 45 +++++------ .../net/corda/loadtest/tests/CrossCashTest.kt | 2 +- .../net/corda/loadtest/tests/NotaryTest.kt | 4 +- .../net/corda/loadtest/tests/SelfIssueTest.kt | 4 +- .../net/corda/verifier/GeneratedLedger.kt | 8 +- .../kotlin/net/corda/verifier/Verifier.kt | 14 ++-- 19 files changed, 216 insertions(+), 209 deletions(-) create mode 100644 core/src/main/kotlin/net/corda/core/utilities/Try.kt diff --git a/client/mock/src/main/kotlin/net/corda/client/mock/Generator.kt b/client/mock/src/main/kotlin/net/corda/client/mock/Generator.kt index 74c052e723..9748e2a2ca 100644 --- a/client/mock/src/main/kotlin/net/corda/client/mock/Generator.kt +++ b/client/mock/src/main/kotlin/net/corda/client/mock/Generator.kt @@ -1,7 +1,7 @@ package net.corda.client.mock import net.corda.client.mock.Generator.Companion.choice -import net.corda.core.ErrorOr +import net.corda.core.utilities.Try import java.util.* /** @@ -12,7 +12,7 @@ import java.util.* * [Generator.choice] picks a generator from the specified list and runs that. * [Generator.frequency] is similar to [choice] but the probability may be specified for each generator (it is normalised before picking). * [Generator.combine] combines two generators of A and B with a function (A, B) -> C. Variants exist for other arities. - * [Generator.bind] sequences two generators using an arbitrary A->Generator function. Keep the usage of this + * [Generator.flatMap] sequences two generators using an arbitrary A->Generator function. Keep the usage of this * function minimal as it may explode the stack, especially when using recursion. * * There are other utilities as well, the type of which are usually descriptive. @@ -31,7 +31,7 @@ import java.util.* * * The above will generate a random list of animals. */ -class Generator(val generate: (SplittableRandom) -> ErrorOr) { +class Generator(val generate: (SplittableRandom) -> Try) { // Functor fun map(function: (A) -> B): Generator = @@ -54,18 +54,19 @@ class Generator(val generate: (SplittableRandom) -> ErrorOr) { product(other1.product(other2.product(other3.product(other4.product(pure({ e -> { d -> { c -> { b -> { a -> function(a, b, c, d, e) } } } } })))))) // Monad - fun bind(function: (A) -> Generator) = - Generator { generate(it).bind { a -> function(a).generate(it) } } + fun flatMap(function: (A) -> Generator): Generator { + return Generator { random -> generate(random).flatMap { function(it).generate(random) } } + } companion object { - fun pure(value: A) = Generator { ErrorOr(value) } - fun impure(valueClosure: () -> A) = Generator { ErrorOr(valueClosure()) } - fun fail(error: Exception) = Generator { ErrorOr.of(error) } + fun pure(value: A) = Generator { Try.Success(value) } + fun impure(valueClosure: () -> A) = Generator { Try.Success(valueClosure()) } + fun fail(error: Exception) = Generator { Try.Failure(error) } // Alternative - fun choice(generators: List>) = intRange(0, generators.size - 1).bind { generators[it] } + fun choice(generators: List>) = intRange(0, generators.size - 1).flatMap { generators[it] } - fun success(generate: (SplittableRandom) -> A) = Generator { ErrorOr(generate(it)) } + fun success(generate: (SplittableRandom) -> A) = Generator { Try.Success(generate(it)) } fun frequency(generators: List>>): Generator { val ranges = mutableListOf>() var current = 0.0 @@ -74,11 +75,11 @@ class Generator(val generate: (SplittableRandom) -> ErrorOr) { ranges.add(Pair(current, next)) current = next } - return doubleRange(0.0, current).bind { value -> - generators[ranges.binarySearch { range -> - if (value < range.first) { + return doubleRange(0.0, current).flatMap { value -> + generators[ranges.binarySearch { (first, second) -> + if (value < first) { 1 - } else if (value < range.second) { + } else if (value < second) { 0 } else { -1 @@ -91,14 +92,12 @@ class Generator(val generate: (SplittableRandom) -> ErrorOr) { val result = mutableListOf() for (generator in generators) { val element = generator.generate(it) - val v = element.value - if (v != null) { - result.add(v) - } else { - return@Generator ErrorOr.of(element.error!!) + when (element) { + is Try.Success -> result.add(element.value) + is Try.Failure -> return@Generator element } } - ErrorOr(result) + Try.Success(result) } } } @@ -109,11 +108,9 @@ fun Generator.generateOrFail(random: SplittableRandom, numberOfTries: Int var error: Throwable? = null for (i in 0..numberOfTries - 1) { val result = generate(random) - val v = result.value - if (v != null) { - return v - } else { - error = result.error + error = when (result) { + is Try.Success -> return result.value + is Try.Failure -> result.exception } } if (error == null) { @@ -147,9 +144,9 @@ fun Generator.Companion.doubleRange(from: Double, to: Double): Generator fun Generator.Companion.char() = Generator { val codePoint = Math.abs(it.nextInt()) % (17 * (1 shl 16)) if (Character.isValidCodePoint(codePoint)) { - return@Generator ErrorOr(codePoint.toChar()) + return@Generator Try.Success(codePoint.toChar()) } else { - ErrorOr.of(IllegalStateException("Could not generate valid codepoint")) + Try.Failure(IllegalStateException("Could not generate valid codepoint")) } } @@ -175,20 +172,19 @@ fun Generator.Companion.replicatePoisson(meanSize: Double, generator: Genera val result = mutableListOf() var finish = false while (!finish) { - val errorOr = Generator.doubleRange(0.0, 1.0).generate(it).bind { value -> + val result = Generator.doubleRange(0.0, 1.0).generate(it).flatMap { value -> if (value < chance) { generator.generate(it).map { result.add(it) } } else { finish = true - ErrorOr(Unit) + Try.Success(Unit) } } - val e = errorOr.error - if (e != null) { - return@Generator ErrorOr.of(e) + if (result is Try.Failure) { + return@Generator result } } - ErrorOr(result) + Try.Success(result) } fun Generator.Companion.pickOne(list: List) = Generator.intRange(0, list.size - 1).map { list[it] } @@ -211,7 +207,7 @@ fun Generator.Companion.pickN(number: Int, list: List) = Generator Generator.Companion.sampleBernoulli(maxRatio: Double = 1.0, vararg collection: A) = diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt index a76ae661ad..ac524995c7 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt @@ -8,15 +8,19 @@ import com.esotericsoftware.kryo.pool.KryoPool import com.google.common.util.concurrent.Futures import net.corda.client.rpc.internal.RPCClient import net.corda.client.rpc.internal.RPCClientConfiguration -import net.corda.core.* import net.corda.core.crypto.random63BitValue +import net.corda.core.future +import net.corda.core.getOrThrow import net.corda.core.messaging.RPCOps +import net.corda.core.millis +import net.corda.core.seconds import net.corda.core.utilities.NetworkHostAndPort -import net.corda.testing.driver.poll +import net.corda.core.utilities.Try import net.corda.node.services.messaging.RPCServerConfiguration import net.corda.nodeapi.RPCApi import net.corda.nodeapi.RPCKryo import net.corda.testing.* +import net.corda.testing.driver.poll import org.apache.activemq.artemis.api.core.SimpleString import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue @@ -25,7 +29,10 @@ import rx.Observable import rx.subjects.PublishSubject import rx.subjects.UnicastSubject import java.time.Duration -import java.util.concurrent.* +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger class RPCStabilityTests { @@ -78,9 +85,9 @@ class RPCStabilityTests { val executor = Executors.newScheduledThreadPool(1) fun startAndStop() { rpcDriver { - ErrorOr.catch { startRpcClient(NetworkHostAndPort("localhost", 9999)).get() } + Try.on { startRpcClient(NetworkHostAndPort("localhost", 9999)).get() } val server = startRpcServer(ops = DummyOps) - ErrorOr.catch { startRpcClient( + Try.on { startRpcClient( server.get().broker.hostAndPort!!, configuration = RPCClientConfiguration.default.copy(minimumServerProtocolVersion = 1) ).get() } diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt index 9d38896c19..e83363b7fc 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt @@ -12,9 +12,9 @@ import com.google.common.cache.RemovalListener import com.google.common.util.concurrent.SettableFuture import com.google.common.util.concurrent.ThreadFactoryBuilder import net.corda.core.ThreadBox +import net.corda.core.crypto.random63BitValue import net.corda.core.getOrThrow import net.corda.core.messaging.RPCOps -import net.corda.core.crypto.random63BitValue import net.corda.core.serialization.KryoPoolWithContext import net.corda.core.utilities.* import net.corda.nodeapi.* @@ -229,14 +229,15 @@ class RPCClientProxyHandler( if (replyFuture == null) { log.error("RPC reply arrived to unknown RPC ID ${serverToClient.id}, this indicates an internal RPC error.") } else { - val rpcCallSite = callSiteMap?.get(serverToClient.id.toLong) - serverToClient.result.match( - onError = { - if (rpcCallSite != null) addRpcCallSiteToThrowable(it, rpcCallSite) - replyFuture.setException(it) - }, - onValue = { replyFuture.set(it) } - ) + val result = serverToClient.result + when (result) { + is Try.Success -> replyFuture.set(result.value) + is Try.Failure -> { + val rpcCallSite = callSiteMap?.get(serverToClient.id.toLong) + if (rpcCallSite != null) addRpcCallSiteToThrowable(result.exception, rpcCallSite) + replyFuture.setException(result.exception) + } + } } } is RPCApi.ServerToClient.Observation -> { diff --git a/core/src/main/kotlin/net/corda/core/Utils.kt b/core/src/main/kotlin/net/corda/core/Utils.kt index 8bd165d370..24d8247df0 100644 --- a/core/src/main/kotlin/net/corda/core/Utils.kt +++ b/core/src/main/kotlin/net/corda/core/Utils.kt @@ -23,7 +23,10 @@ import java.nio.file.* import java.nio.file.attribute.FileAttribute import java.time.Duration import java.time.temporal.Temporal -import java.util.concurrent.* +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ExecutionException +import java.util.concurrent.Future +import java.util.concurrent.TimeUnit import java.util.concurrent.locks.ReentrantLock import java.util.stream.Stream import java.util.zip.Deflater @@ -324,63 +327,6 @@ data class InputStreamAndHash(val inputStream: InputStream, val sha256: SecureHa val Throwable.rootCause: Throwable get() = Throwables.getRootCause(this) -/** Representation of an operation that may have thrown an error. */ -@Suppress("DataClassPrivateConstructor") -@CordaSerializable -data class ErrorOr private constructor(val value: A?, val error: Throwable?) { - // The ErrorOr holds a value iff error == null - constructor(value: A) : this(value, null) - - companion object { - /** Runs the given lambda and wraps the result. */ - inline fun catch(body: () -> T): ErrorOr { - return try { - ErrorOr(body()) - } catch (t: Throwable) { - ErrorOr.of(t) - } - } - - fun of(t: Throwable) = ErrorOr(null, t) - } - - fun match(onValue: (A) -> T, onError: (Throwable) -> T): T { - if (error == null) { - return onValue(value as A) - } else { - return onError(error) - } - } - - fun getOrThrow(): A { - if (error == null) { - return value as A - } else { - throw error - } - } - - // Functor - fun map(function: (A) -> B) = ErrorOr(value?.let(function), error) - - // Applicative - fun combine(other: ErrorOr, function: (A, B) -> C): ErrorOr { - val newError = error ?: other.error - return ErrorOr(if (newError != null) null else function(value as A, other.value as B), newError) - } - - // Monad - fun bind(function: (A) -> ErrorOr): ErrorOr { - return if (error == null) { - function(value as A) - } else { - ErrorOr.of(error) - } - } - - fun mapError(function: (Throwable) -> Throwable) = ErrorOr(value, error?.let(function)) -} - /** * Returns an Observable that buffers events until subscribed. * @see UnicastSubject diff --git a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt index fc5d9d0b4a..641b1c28e2 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt @@ -1,7 +1,6 @@ package net.corda.core.messaging import com.google.common.util.concurrent.ListenableFuture -import net.corda.core.ErrorOr import net.corda.core.contracts.Amount import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateAndRef @@ -19,6 +18,7 @@ import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.Sort import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.Try import org.bouncycastle.asn1.x500.X500Name import rx.Observable import java.io.InputStream @@ -44,7 +44,7 @@ sealed class StateMachineUpdate { override val id: StateMachineRunId get() = stateMachineInfo.id } - data class Removed(override val id: StateMachineRunId, val result: ErrorOr<*>) : StateMachineUpdate() + data class Removed(override val id: StateMachineRunId, val result: Try<*>) : StateMachineUpdate() } @CordaSerializable diff --git a/core/src/main/kotlin/net/corda/core/utilities/Try.kt b/core/src/main/kotlin/net/corda/core/utilities/Try.kt new file mode 100644 index 0000000000..74c7833e66 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/utilities/Try.kt @@ -0,0 +1,74 @@ +package net.corda.core.utilities + +import net.corda.core.serialization.CordaSerializable +import net.corda.core.utilities.Try.Failure +import net.corda.core.utilities.Try.Success + +/** + * Representation of an operation that has either succeeded with a result (represented by [Success]) or failed with an + * exception (represented by [Failure]). + */ +@CordaSerializable +sealed class Try { + companion object { + /** + * Executes the given block of code and returns a [Success] capturing the result, or a [Failure] if an exception + * is thrown. + */ + @JvmStatic + inline fun on(body: () -> T): Try { + return try { + Success(body()) + } catch (t: Throwable) { + Failure(t) + } + } + } + + /** Returns `true` iff the [Try] is a [Success]. */ + abstract val isFailure: Boolean + + /** Returns `true` iff the [Try] is a [Failure]. */ + abstract val isSuccess: Boolean + + /** Returns the value if a [Success] otherwise throws the exception if a [Failure]. */ + abstract fun getOrThrow(): A + + /** Maps the given function to the value from this [Success], or returns `this` if this is a [Failure]. */ + inline fun map(function: (A) -> B): Try = when (this) { + is Success -> Success(function(value)) + is Failure -> this + } + + /** Returns the given function applied to the value from this [Success], or returns `this` if this is a [Failure]. */ + inline fun flatMap(function: (A) -> Try): Try = when (this) { + is Success -> function(value) + is Failure -> this + } + + /** + * Maps the given function to the values from this [Success] and [other], or returns `this` if this is a [Failure] + * or [other] if [other] is a [Failure]. + */ + inline fun combine(other: Try, function: (A, B) -> C): Try = when (this) { + is Success -> when (other) { + is Success -> Success(function(value, other.value)) + is Failure -> other + } + is Failure -> this + } + + data class Success(val value: A) : Try() { + override val isSuccess: Boolean get() = true + override val isFailure: Boolean get() = false + override fun getOrThrow(): A = value + override fun toString(): String = "Success($value)" + } + + data class Failure(val exception: Throwable) : Try() { + override val isSuccess: Boolean get() = false + override val isFailure: Boolean get() = true + override fun getOrThrow(): Nothing = throw exception + override fun toString(): String = "Failure($exception)" + } +} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt b/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt index 836315752a..8e5aaf8cfb 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt @@ -1,13 +1,14 @@ package net.corda.nodeapi import com.esotericsoftware.kryo.pool.KryoPool -import net.corda.core.ErrorOr import net.corda.core.serialization.KryoPoolWithContext import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize +import net.corda.core.utilities.Try import net.corda.nodeapi.RPCApi.ClientToServer import net.corda.nodeapi.RPCApi.ObservableId import net.corda.nodeapi.RPCApi.RPC_CLIENT_BINDING_REMOVALS +import net.corda.nodeapi.RPCApi.RPC_CLIENT_BINDING_REMOVAL_FILTER_EXPRESSION import net.corda.nodeapi.RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX import net.corda.nodeapi.RPCApi.RPC_SERVER_QUEUE_NAME import net.corda.nodeapi.RPCApi.RpcRequestId @@ -151,7 +152,7 @@ object RPCApi { data class RpcReply( val id: RpcRequestId, - val result: ErrorOr + val result: Try ) : ServerToClient() { override fun writeToClientMessage(kryoPool: KryoPool, message: ClientMessage) { message.putIntProperty(TAG_FIELD_NAME, Tag.RPC_REPLY.ordinal) diff --git a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt index 5f257894e4..c01b7e7fd9 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt @@ -1,11 +1,9 @@ package net.corda.node.services import com.nhaarman.mockito_kotlin.whenever -import net.corda.core.ErrorOr import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateRef import net.corda.core.contracts.TransactionType -import net.corda.testing.contracts.DummyContract import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.SecureHash import net.corda.core.div @@ -13,6 +11,7 @@ import net.corda.core.getOrThrow import net.corda.core.identity.Party import net.corda.core.node.services.ServiceInfo import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.Try import net.corda.flows.NotaryError import net.corda.flows.NotaryException import net.corda.flows.NotaryFlow @@ -23,10 +22,10 @@ import net.corda.node.services.transactions.minClusterSize import net.corda.node.services.transactions.minCorrectReplicas import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.node.utilities.transaction +import net.corda.testing.contracts.DummyContract import net.corda.testing.node.MockNetwork import org.bouncycastle.asn1.x500.X500Name import org.junit.After -import org.junit.Ignore import org.junit.Test import java.nio.file.Files import kotlin.test.assertEquals @@ -95,10 +94,10 @@ class BFTNotaryServiceTests { val flows = spendTxs.map { NotaryFlow.Client(it) } val stateMachines = flows.map { services.startFlow(it) } mockNet.runNetwork() - val results = stateMachines.map { ErrorOr.catch { it.resultFuture.getOrThrow() } } + val results = stateMachines.map { Try.on { it.resultFuture.getOrThrow() } } val successfulIndex = results.mapIndexedNotNull { index, result -> - if (result.error == null) { - val signers = result.getOrThrow().map { it.by } + if (result is Try.Success) { + val signers = result.value.map { it.by } assertEquals(minCorrectReplicas(clusterSize), signers.size) signers.forEach { assertTrue(it in (notary.owningKey as CompositeKey).leafKeys) @@ -109,8 +108,8 @@ class BFTNotaryServiceTests { } }.single() spendTxs.zip(results).forEach { (tx, result) -> - if (result.error != null) { - val error = (result.error as NotaryException).error as NotaryError.Conflict + if (result is Try.Failure) { + val error = (result.exception as NotaryException).error as NotaryError.Conflict assertEquals(tx.id, error.txId) val (stateRef, consumingTx) = error.conflict.verified().stateHistory.entries.single() assertEquals(StateRef(issueTx.id, 0), stateRef) diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt index 05c13f6341..71ee340dbb 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt @@ -12,15 +12,11 @@ import com.google.common.collect.HashMultimap import com.google.common.collect.Multimaps import com.google.common.collect.SetMultimap import com.google.common.util.concurrent.ThreadFactoryBuilder -import net.corda.core.ErrorOr -import net.corda.core.messaging.RPCOps import net.corda.core.crypto.random63BitValue +import net.corda.core.messaging.RPCOps import net.corda.core.seconds import net.corda.core.serialization.KryoPoolWithContext -import net.corda.core.utilities.LazyStickyPool -import net.corda.core.utilities.LifeCycle -import net.corda.core.utilities.debug -import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.* import net.corda.node.services.RPCUserService import net.corda.nodeapi.* import net.corda.nodeapi.ArtemisMessagingComponent.Companion.NODE_USER @@ -43,7 +39,6 @@ import java.lang.reflect.InvocationTargetException import java.lang.reflect.Method import java.time.Duration import java.util.concurrent.* -import kotlin.collections.ArrayList data class RPCServerConfiguration( /** The number of threads to use for handling RPC requests */ @@ -270,14 +265,7 @@ class RPCServer( ) rpcExecutor!!.submit { val result = invokeRpc(rpcContext, clientToServer.methodName, clientToServer.arguments) - val resultWithExceptionUnwrapped = result.mapError { - if (it is InvocationTargetException) { - it.cause ?: RPCException("Caught InvocationTargetException without cause") - } else { - it - } - } - sendReply(clientToServer.id, clientToServer.clientAddress, resultWithExceptionUnwrapped) + sendReply(clientToServer.id, clientToServer.clientAddress, result) } } is RPCApi.ClientToServer.ObservablesClosed -> { @@ -287,25 +275,24 @@ class RPCServer( artemisMessage.acknowledge() } - private fun invokeRpc(rpcContext: RpcContext, methodName: String, arguments: List): ErrorOr { - return ErrorOr.catch { + private fun invokeRpc(rpcContext: RpcContext, methodName: String, arguments: List): Try { + return Try.on { try { CURRENT_RPC_CONTEXT.set(rpcContext) log.debug { "Calling $methodName" } val method = methodTable[methodName] ?: throw RPCException("Received RPC for unknown method $methodName - possible client/server version skew?") method.invoke(ops, *arguments.toTypedArray()) + } catch (e: InvocationTargetException) { + throw e.cause ?: RPCException("Caught InvocationTargetException without cause") } finally { CURRENT_RPC_CONTEXT.remove() } } } - private fun sendReply(requestId: RPCApi.RpcRequestId, clientAddress: SimpleString, resultWithExceptionUnwrapped: ErrorOr) { - val reply = RPCApi.ServerToClient.RpcReply( - id = requestId, - result = resultWithExceptionUnwrapped - ) + private fun sendReply(requestId: RPCApi.RpcRequestId, clientAddress: SimpleString, result: Try) { + val reply = RPCApi.ServerToClient.RpcReply(requestId, result) val observableContext = ObservableContext( requestId, observableMap, diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt index 537597f833..b8b5825744 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt @@ -7,18 +7,14 @@ import co.paralleluniverse.strands.Strand import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.SettableFuture import net.corda.core.DeclaredField.Companion.declaredField -import net.corda.core.ErrorOr import net.corda.core.abbreviate import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.random63BitValue import net.corda.core.flows.* import net.corda.core.identity.Party import net.corda.core.internal.FlowStateMachine -import net.corda.core.crypto.random63BitValue import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.ProgressTracker -import net.corda.core.utilities.UntrustworthyData -import net.corda.core.utilities.debug -import net.corda.core.utilities.trace +import net.corda.core.utilities.* import net.corda.node.services.api.FlowAppAuditEvent import net.corda.node.services.api.FlowPermissionAuditEvent import net.corda.node.services.api.ServiceHubInternal @@ -71,7 +67,7 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, @Transient override lateinit var serviceHub: ServiceHubInternal @Transient internal lateinit var database: Database @Transient internal lateinit var actionOnSuspend: (FlowIORequest) -> Unit - @Transient internal lateinit var actionOnEnd: (ErrorOr, Boolean) -> Unit + @Transient internal lateinit var actionOnEnd: (Try, Boolean) -> Unit @Transient internal var fromCheckpoint: Boolean = false @Transient private var txTrampoline: Transaction? = null @@ -125,7 +121,7 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, .filter { it.state is FlowSessionState.Initiating } .forEach { it.waitForConfirmation() } // This is to prevent actionOnEnd being called twice if it throws an exception - actionOnEnd(ErrorOr(result), false) + actionOnEnd(Try.Success(result), false) _resultFuture?.set(result) logic.progressTracker?.currentStep = ProgressTracker.DONE logger.debug { "Flow finished with result $result" } @@ -138,7 +134,7 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, } private fun processException(exception: Throwable, propagated: Boolean) { - actionOnEnd(ErrorOr.of(exception), propagated) + actionOnEnd(Try.Failure(exception), propagated) _resultFuture?.setException(exception) logic.progressTracker?.endWithError(exception) } diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt index 53e8a460eb..27ce753cd3 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt @@ -15,7 +15,8 @@ import com.google.common.collect.HashMultimap import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.MoreExecutors import io.requery.util.CloseableIterator -import net.corda.core.* +import net.corda.core.ThreadBox +import net.corda.core.bufferUntilSubscribed import net.corda.core.crypto.SecureHash import net.corda.core.crypto.random63BitValue import net.corda.core.flows.FlowException @@ -25,6 +26,8 @@ import net.corda.core.flows.StateMachineRunId import net.corda.core.identity.Party import net.corda.core.messaging.DataFeed import net.corda.core.serialization.* +import net.corda.core.then +import net.corda.core.utilities.Try import net.corda.core.utilities.debug import net.corda.core.utilities.loggerFor import net.corda.core.utilities.trace @@ -122,7 +125,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, abstract val logic: FlowLogic<*> data class Add(override val logic: FlowLogic<*>) : Change() - data class Removed(override val logic: FlowLogic<*>, val result: ErrorOr<*>) : Change() + data class Removed(override val logic: FlowLogic<*>, val result: Try<*>) : Change() } // A list of all the state machines being managed by this class. We expose snapshots of it via the stateMachines @@ -442,13 +445,13 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, processIORequest(ioRequest) decrementLiveFibers() } - fiber.actionOnEnd = { resultOrError, propagated -> + fiber.actionOnEnd = { result, propagated -> try { mutex.locked { stateMachines.remove(fiber)?.let { checkpointStorage.removeCheckpoint(it) } - notifyChangeObservers(Change.Removed(fiber.logic, resultOrError)) + notifyChangeObservers(Change.Removed(fiber.logic, result)) } - endAllFiberSessions(fiber, resultOrError.error, propagated) + endAllFiberSessions(fiber, result, propagated) } finally { fiber.commitTransaction() decrementLiveFibers() @@ -463,10 +466,10 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, } } - private fun endAllFiberSessions(fiber: FlowStateMachineImpl<*>, exception: Throwable?, propagated: Boolean) { + private fun endAllFiberSessions(fiber: FlowStateMachineImpl<*>, result: Try<*>, propagated: Boolean) { openSessions.values.removeIf { session -> if (session.fiber == fiber) { - session.endSession(exception, propagated) + session.endSession((result as? Try.Failure)?.exception, propagated) true } else { false diff --git a/node/src/main/kotlin/net/corda/node/shell/FlowWatchPrintingSubscriber.kt b/node/src/main/kotlin/net/corda/node/shell/FlowWatchPrintingSubscriber.kt index fa6b477321..f249caa619 100644 --- a/node/src/main/kotlin/net/corda/node/shell/FlowWatchPrintingSubscriber.kt +++ b/node/src/main/kotlin/net/corda/node/shell/FlowWatchPrintingSubscriber.kt @@ -1,20 +1,22 @@ package net.corda.node.shell import com.google.common.util.concurrent.SettableFuture -import net.corda.core.ErrorOr import net.corda.core.crypto.commonName import net.corda.core.flows.FlowInitiator import net.corda.core.flows.StateMachineRunId import net.corda.core.messaging.StateMachineUpdate +import net.corda.core.messaging.StateMachineUpdate.Added +import net.corda.core.messaging.StateMachineUpdate.Removed import net.corda.core.then import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.Try import org.crsh.text.Color import org.crsh.text.Decoration import org.crsh.text.RenderPrintWriter import org.crsh.text.ui.LabelElement -import org.crsh.text.ui.TableElement import org.crsh.text.ui.Overflow import org.crsh.text.ui.RowElement +import org.crsh.text.ui.TableElement import rx.Subscriber class FlowWatchPrintingSubscriber(private val toStream: RenderPrintWriter) : Subscriber() { @@ -51,10 +53,10 @@ class FlowWatchPrintingSubscriber(private val toStream: RenderPrintWriter) : Sub future.setException(e) } - private fun stateColor(smmUpdate: StateMachineUpdate): Color { - return when(smmUpdate){ - is StateMachineUpdate.Added -> Color.blue - is StateMachineUpdate.Removed -> smmUpdate.result.match({ Color.green } , { Color.red }) + private fun stateColor(update: StateMachineUpdate): Color { + return when (update) { + is Added -> Color.blue + is Removed -> if (update.result.isSuccess) Color.green else Color.red } } @@ -68,7 +70,7 @@ class FlowWatchPrintingSubscriber(private val toStream: RenderPrintWriter) : Sub // TODO Add progress tracker? private fun createStateMachinesRow(smmUpdate: StateMachineUpdate) { when (smmUpdate) { - is StateMachineUpdate.Added -> { + is Added -> { table.add(RowElement().add( LabelElement(formatFlowId(smmUpdate.id)), LabelElement(formatFlowName(smmUpdate.stateMachineInfo.flowLogicClassName)), @@ -77,7 +79,7 @@ class FlowWatchPrintingSubscriber(private val toStream: RenderPrintWriter) : Sub ).style(stateColor(smmUpdate).fg())) indexMap[smmUpdate.id] = table.rows.size - 1 } - is StateMachineUpdate.Removed -> { + is Removed -> { val idx = indexMap[smmUpdate.id] if (idx != null) { val oldRow = table.rows[idx] @@ -114,7 +116,7 @@ class FlowWatchPrintingSubscriber(private val toStream: RenderPrintWriter) : Sub } } - private fun formatFlowResult(flowResult: ErrorOr<*>): String { + private fun formatFlowResult(flowResult: Try<*>): String { fun successFormat(value: Any?): String { return when(value) { is SignedTransaction -> "Tx ID: " + value.id.toString() @@ -123,6 +125,9 @@ class FlowWatchPrintingSubscriber(private val toStream: RenderPrintWriter) : Sub else -> value.toString() } } - return flowResult.match({ successFormat(it) }, { it.message ?: it.toString() }) + return when (flowResult) { + is Try.Success -> successFormat(flowResult.value) + is Try.Failure -> flowResult.exception.message ?: flowResult.exception.toString() + } } } diff --git a/node/src/main/kotlin/net/corda/node/utilities/AddOrRemove.kt b/node/src/main/kotlin/net/corda/node/utilities/AddOrRemove.kt index 0cf5538a95..155e7853d2 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/AddOrRemove.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/AddOrRemove.kt @@ -1,6 +1,5 @@ package net.corda.node.utilities -import net.corda.core.ErrorOr import net.corda.core.serialization.CordaSerializable /** diff --git a/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt b/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt index c176242b41..809f3fd4b6 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -19,10 +19,7 @@ import net.corda.core.messaging.CordaRPCOps import net.corda.core.node.NodeInfo import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceType -import net.corda.core.utilities.NetworkHostAndPort -import net.corda.core.utilities.WHITESPACE -import net.corda.core.utilities.loggerFor -import net.corda.core.utilities.parseNetworkHostAndPort +import net.corda.core.utilities.* import net.corda.node.internal.Node import net.corda.node.internal.NodeStartup import net.corda.node.serialization.NodeClock @@ -352,15 +349,16 @@ fun poll( if (++counter == warnCount) { log.warn("Been polling $pollName for ${pollInterval.multipliedBy(warnCount.toLong()).seconds} seconds...") } - ErrorOr.catch(check).match(onValue = { - if (it != null) { - resultFuture.set(it) + try { + val checkResult = check() + if (checkResult != null) { + resultFuture.set(checkResult) } else { executorService.schedule(this, pollInterval.toMillis(), MILLISECONDS) } - }, onError = { - resultFuture.setException(it) - }) + } catch (t: Throwable) { + resultFuture.setException(t) + } } } executorService.submit(task) // The check may be expensive, so always run it in the background even the first time. @@ -389,7 +387,7 @@ class ShutdownManager(private val executorService: ExecutorService) { } fun shutdown() { - val shutdownFutures = state.locked { + val shutdownActionFutures = state.locked { if (isShutdown) { emptyList Unit>>() } else { @@ -397,21 +395,16 @@ class ShutdownManager(private val executorService: ExecutorService) { registeredShutdowns } } - val shutdowns = shutdownFutures.map { ErrorOr.catch { it.getOrThrow(1.seconds) } } - shutdowns.reversed().forEach { errorOrShutdown -> - errorOrShutdown.match( - onValue = { shutdown -> - try { - shutdown() - } catch (throwable: Throwable) { - log.error("Exception while shutting down", throwable) - } - }, - onError = { error -> - log.error("Exception while getting shutdown method, disregarding", error) - } - ) - } + val shutdowns = shutdownActionFutures.map { Try.on { it.getOrThrow(1.seconds) } } + shutdowns.reversed().forEach { when (it) { + is Try.Success -> + try { + it.value() + } catch (t: Throwable) { + log.warn("Exception while shutting down", t) + } + is Try.Failure -> log.warn("Exception while getting shutdown method, disregarding", it.exception) + } } } fun registerShutdown(shutdown: ListenableFuture<() -> Unit>) { diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/CrossCashTest.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/CrossCashTest.kt index 6753020d98..f2d90594ab 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/CrossCashTest.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/CrossCashTest.kt @@ -117,7 +117,7 @@ val crossCashTest = LoadTest( generate = { (nodeVaults), parallelism -> val nodeMap = simpleNodes.associateBy { it.info.legalIdentity } val anonymous = true - Generator.pickN(parallelism, simpleNodes).bind { nodes -> + Generator.pickN(parallelism, simpleNodes).flatMap { nodes -> Generator.sequence( nodes.map { node -> val quantities = nodeVaults[node.info.legalIdentity] ?: mapOf() diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt index b9440ee675..f684b08bb4 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt @@ -6,7 +6,6 @@ import net.corda.client.mock.pickOne import net.corda.client.mock.replicate import net.corda.contracts.asset.DUMMY_CASH_ISSUER import net.corda.contracts.asset.DUMMY_CASH_ISSUER_KEY -import net.corda.testing.contracts.DummyContract import net.corda.core.flows.FlowException import net.corda.core.messaging.startFlow import net.corda.core.thenMatch @@ -14,6 +13,7 @@ import net.corda.core.transactions.SignedTransaction import net.corda.flows.FinalityFlow import net.corda.loadtest.LoadTest import net.corda.loadtest.NodeConnection +import net.corda.testing.contracts.DummyContract import net.corda.testing.node.MockServices import org.slf4j.LoggerFactory @@ -25,7 +25,7 @@ val dummyNotarisationTest = LoadTest( "Notarising dummy transactions", generate = { _, _ -> val issuerServices = MockServices(DUMMY_CASH_ISSUER_KEY) - val generateTx = Generator.pickOne(simpleNodes).bind { node -> + val generateTx = Generator.pickOne(simpleNodes).flatMap { node -> Generator.int().map { val issueBuilder = DummyContract.generateInitial(it, notary.info.notaryIdentity, DUMMY_CASH_ISSUER) val issueTx = issuerServices.signInitialTransaction(issueBuilder) diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/SelfIssueTest.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/SelfIssueTest.kt index 0543cd83b4..61194d7ea8 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/SelfIssueTest.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/SelfIssueTest.kt @@ -37,12 +37,12 @@ val selfIssueTest = LoadTest( "Self issuing cash randomly", generate = { _, parallelism -> - val generateIssue = Generator.pickOne(simpleNodes).bind { node -> + val generateIssue = Generator.pickOne(simpleNodes).flatMap { node -> generateIssue(1000, USD, notary.info.notaryIdentity, listOf(node.info.legalIdentity), anonymous = true).map { SelfIssueCommand(it, node) } } - Generator.replicatePoisson(parallelism.toDouble(), generateIssue).bind { + Generator.replicatePoisson(parallelism.toDouble(), generateIssue).flatMap { // We need to generate at least one if (it.isEmpty()) { Generator.sequence(listOf(generateIssue)) diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt index fafc621c2e..3c0ccd7460 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt @@ -58,7 +58,7 @@ data class GeneratedLedger( * Invariants: The input list must be empty. */ val issuanceGenerator: Generator> by lazy { - val outputsGen = outputsGenerator.bind { outputs -> + val outputsGen = outputsGenerator.flatMap { outputs -> Generator.sequence( outputs.map { output -> pickOneOrMaybeNew(identities, partyGenerator).map { notary -> @@ -140,7 +140,7 @@ data class GeneratedLedger( fun notaryChangeTransactionGenerator(inputNotary: Party, inputsToChooseFrom: List>): Generator> { val newNotaryGen = pickOneOrMaybeNew(identities - inputNotary, partyGenerator) val inputsGen = Generator.sampleBernoulli(inputsToChooseFrom) - return inputsGen.bind { inputs -> + return inputsGen.flatMap { inputs -> val signers: List = (inputs.flatMap { it.state.data.participants } + inputNotary).map { it.owningKey } val outputsGen = Generator.sequence(inputs.map { input -> newNotaryGen.map { TransactionState(input.state.data, it, null) } }) outputsGen.combine(attachmentsGenerator) { outputs, txAttachments -> @@ -177,7 +177,7 @@ data class GeneratedLedger( if (availableOutputs.isEmpty()) { issuanceGenerator } else { - Generator.pickOne(availableOutputs.keys.toList()).bind { inputNotary -> + Generator.pickOne(availableOutputs.keys.toList()).flatMap { inputNotary -> val inputsToChooseFrom = availableOutputs[inputNotary]!! Generator.frequency( 0.3 to issuanceGenerator, @@ -231,7 +231,7 @@ fun pickOneOrMaybeNew(from: Collection, generator: Generator): Generat if (from.isEmpty()) { return generator } else { - return generator.bind { + return generator.flatMap { Generator.pickOne(from + it) } } diff --git a/verifier/src/main/kotlin/net/corda/verifier/Verifier.kt b/verifier/src/main/kotlin/net/corda/verifier/Verifier.kt index ea54765408..b8df7f891b 100644 --- a/verifier/src/main/kotlin/net/corda/verifier/Verifier.kt +++ b/verifier/src/main/kotlin/net/corda/verifier/Verifier.kt @@ -3,8 +3,6 @@ package net.corda.verifier import com.typesafe.config.Config import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigParseOptions -import net.corda.core.ErrorOr -import net.corda.nodeapi.internal.addShutdownHook import net.corda.core.div import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.debug @@ -15,6 +13,7 @@ import net.corda.nodeapi.VerifierApi import net.corda.nodeapi.VerifierApi.VERIFICATION_REQUESTS_QUEUE_NAME import net.corda.nodeapi.config.NodeSSLConfiguration import net.corda.nodeapi.config.getValue +import net.corda.nodeapi.internal.addShutdownHook import org.apache.activemq.artemis.api.core.client.ActiveMQClient import java.nio.file.Path import java.nio.file.Paths @@ -61,14 +60,15 @@ class Verifier { consumer.setMessageHandler { val request = VerifierApi.VerificationRequest.fromClientMessage(it) log.debug { "Received verification request with id ${request.verificationId}" } - val result = ErrorOr.catch { + val error = try { request.transaction.verify() - } - if (result.error != null) { - log.debug { "Verification returned with error ${result.error}" } + null + } catch (t: Throwable) { + log.debug("Verification returned with error:", t) + t } val reply = session.createMessage(false) - val response = VerifierApi.VerificationResponse(request.verificationId, result.error) + val response = VerifierApi.VerificationResponse(request.verificationId, error) response.writeToClientMessage(reply) replyProducer.send(request.responseAddress, reply) it.acknowledge() From b6ed3375bf7435661603b8520dbe58bcb3a43cbe Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Tue, 11 Jul 2017 14:06:12 +0100 Subject: [PATCH 85/97] Add more Vim swap file extensions to git ignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index c5ea620339..21f2f41b5c 100644 --- a/.gitignore +++ b/.gitignore @@ -88,6 +88,8 @@ docs/virtualenv/ # vim *.swp +*.swn +*.swo # Files you may find useful to have in your working directory. PLAN From 0b2188d27bcbffa6d2cccfd3c0edca81c87abfe6 Mon Sep 17 00:00:00 2001 From: Matthew Nesbit Date: Tue, 11 Jul 2017 14:13:52 +0100 Subject: [PATCH 86/97] Fix a bug in the wrapper code for the vault. If we dip down to zero subscribers, no future updates are streamed. This hasn't been seen historically, because the cash metrics observer is always present, but this will be moved out of node. --- .../corda/node/utilities/DatabaseSupport.kt | 12 ++++--- .../corda/node/utilities/ObservablesTests.kt | 32 +++++++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/utilities/DatabaseSupport.kt b/node/src/main/kotlin/net/corda/node/utilities/DatabaseSupport.kt index 83c675a7b6..b5f8e62e7c 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/DatabaseSupport.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/DatabaseSupport.kt @@ -6,7 +6,6 @@ import com.zaxxer.hikari.HikariDataSource import net.corda.core.crypto.SecureHash import net.corda.core.crypto.parsePublicKeyBase58 import net.corda.core.crypto.toBase58String -import net.corda.core.identity.PartyAndCertificate import net.corda.node.utilities.StrandLocalTransactionManager.Boundary import org.bouncycastle.cert.X509CertificateHolder import org.h2.jdbc.JdbcBlob @@ -23,7 +22,6 @@ import java.io.Closeable import java.security.PublicKey import java.security.cert.CertPath import java.security.cert.CertificateFactory -import java.security.cert.X509Certificate import java.sql.Connection import java.time.Instant import java.time.LocalDate @@ -256,7 +254,7 @@ private class NoOpSubscriber(t: Subscriber) : Subscriber(t) { * that might be in place. */ fun rx.Observable.wrapWithDatabaseTransaction(db: Database? = null): rx.Observable { - val wrappingSubscriber = DatabaseTransactionWrappingSubscriber(db) + var wrappingSubscriber = DatabaseTransactionWrappingSubscriber(db) // Use lift to add subscribers to a special subscriber that wraps a database transaction around observations. // Each subscriber will be passed to this lambda when they subscribe, at which point we add them to wrapping subscriber. return this.lift { toBeWrappedInDbTx: Subscriber -> @@ -265,7 +263,13 @@ fun rx.Observable.wrapWithDatabaseTransaction(db: Database? = null) // If we are the first subscriber, return the shared subscriber, otherwise return a subscriber that does nothing. if (wrappingSubscriber.delegates.size == 1) wrappingSubscriber else NoOpSubscriber(toBeWrappedInDbTx) // Clean up the shared list of subscribers when they unsubscribe. - }.doOnUnsubscribe { wrappingSubscriber.cleanUp() } + }.doOnUnsubscribe { + wrappingSubscriber.cleanUp() + // If cleanup removed the last subscriber reset the system, as future subscribers might need the stream again + if (wrappingSubscriber.delegates.isEmpty()) { + wrappingSubscriber = DatabaseTransactionWrappingSubscriber(db) + } + } } // Composite columns for use with below Exposed helpers. diff --git a/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt b/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt index 72213773fe..8e0c666796 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt @@ -237,4 +237,36 @@ class ObservablesTests { subscription2.unsubscribe() assertThat(unsubscribed).isTrue() } + + @Test + fun `check wrapping in db tx restarts if we pass through zero subscribers`() { + val database = createDatabase() + + val source = PublishSubject.create() + var unsubscribed = false + + val bufferedObservable: Observable = source.doOnUnsubscribe { unsubscribed = true } + val databaseWrappedObservable: Observable = bufferedObservable.wrapWithDatabaseTransaction(database) + + assertThat(unsubscribed).isFalse() + + val subscription1 = databaseWrappedObservable.subscribe { } + val subscription2 = databaseWrappedObservable.subscribe { } + + subscription1.unsubscribe() + assertThat(unsubscribed).isFalse() + + subscription2.unsubscribe() + assertThat(unsubscribed).isTrue() + + val event = SettableFuture.create() + val subscription3 = databaseWrappedObservable.subscribe { event.set(it) } + + source.onNext(1) + + assertThat(event.isDone).isTrue() + assertThat(event.get()).isEqualTo(1) + + subscription3.unsubscribe() + } } \ No newline at end of file From 057fa0443bc546fdb4e7d38dc27daf3ae66d8254 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Tue, 11 Jul 2017 14:42:22 +0100 Subject: [PATCH 87/97] Remove debug println that snuck past code review --- .../net/corda/core/serialization/carpenter/ClassCarpenter.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/main/kotlin/net/corda/core/serialization/carpenter/ClassCarpenter.kt b/core/src/main/kotlin/net/corda/core/serialization/carpenter/ClassCarpenter.kt index 2544727a9c..5d774df240 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/carpenter/ClassCarpenter.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/carpenter/ClassCarpenter.kt @@ -90,7 +90,6 @@ class ClassCarpenter { get() = if (this.field.isPrimitive) this.descriptor else "Ljava/lang/Object;" fun generateField(cw: ClassWriter) { - println ("generateField $name $nullabilityAnnotation") val fieldVisitor = cw.visitField(ACC_PROTECTED + ACC_FINAL, name, descriptor, null, null) fieldVisitor.visitAnnotation(nullabilityAnnotation, true).visitEnd() fieldVisitor.visitEnd() From d6d5edc33bcf1da3f3b89bd5de88af92e1f712cf Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Tue, 11 Jul 2017 15:30:21 +0100 Subject: [PATCH 88/97] Check vault updates in IssuerFlowTest Modify issuer flow test to verify the consumed/produced states, rather than just checking the transaction matches the value returned via the flow state machine. This is both a simpler and more relevant test. --- .../net/corda/flows/CashPaymentFlowTests.kt | 48 ++++++-- .../kotlin/net/corda/flows/IssuerFlowTest.kt | 115 ++++++++++++------ .../corda/bank/BankOfCordaRPCClientTest.kt | 9 +- 3 files changed, 124 insertions(+), 48 deletions(-) diff --git a/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt b/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt index 5e04b65b67..3bdf21b4da 100644 --- a/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt @@ -5,7 +5,13 @@ import net.corda.core.contracts.DOLLARS import net.corda.core.contracts.`issued by` import net.corda.core.getOrThrow import net.corda.core.identity.Party +import net.corda.core.node.services.Vault +import net.corda.core.node.services.trackBy +import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.utilities.OpaqueBytes +import net.corda.node.utilities.transaction +import net.corda.testing.expect +import net.corda.testing.expectEvents import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork.MockNode @@ -51,15 +57,39 @@ class CashPaymentFlowTests { val payTo = notaryNode.info.legalIdentity val expectedPayment = 500.DOLLARS val expectedChange = 1500.DOLLARS - val future = bankOfCordaNode.services.startFlow(CashPaymentFlow(expectedPayment, - payTo)).resultFuture - mockNet.runNetwork() - val (paymentTx, receipient) = future.getOrThrow() - val states = paymentTx.tx.outputs.map { it.data }.filterIsInstance() - val paymentState: Cash.State = states.single { it.owner == receipient } - val changeState: Cash.State = states.single { it != paymentState } - assertEquals(expectedChange.`issued by`(bankOfCorda.ref(ref)), changeState.amount) - assertEquals(expectedPayment.`issued by`(bankOfCorda.ref(ref)), paymentState.amount) + + bankOfCordaNode.database.transaction { + // Register for vault updates + val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL) + val (_, vaultUpdatesBoc) = bankOfCordaNode.services.vaultQueryService.trackBy(criteria) + val (_, vaultUpdatesBankClient) = notaryNode.services.vaultQueryService.trackBy(criteria) + + val future = bankOfCordaNode.services.startFlow(CashPaymentFlow(expectedPayment, + payTo)).resultFuture + mockNet.runNetwork() + future.getOrThrow() + + // Check Bank of Corda vault updates - we take in some issued cash and split it into $500 to the notary + // and $1,500 back to us, so we expect to consume one state, produce one state for our own vault + vaultUpdatesBoc.expectEvents { + expect { update -> + require(update.consumed.size == 1) { "Expected 1 consumed states, actual: $update" } + require(update.produced.size == 1) { "Expected 1 produced states, actual: $update" } + val changeState = update.produced.single().state.data as Cash.State + assertEquals(expectedChange.`issued by`(bankOfCorda.ref(ref)), changeState.amount) + } + } + + // Check notary node vault updates + vaultUpdatesBankClient.expectEvents { + expect { update -> + require(update.consumed.isEmpty()) { update.consumed.size } + require(update.produced.size == 1) { update.produced.size } + val paymentState = update.produced.single().state.data as Cash.State + assertEquals(expectedPayment.`issued by`(bankOfCorda.ref(ref)), paymentState.amount) + } + } + } } @Test diff --git a/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt b/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt index 97156d6a6d..3007a13a5c 100644 --- a/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt +++ b/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt @@ -1,30 +1,28 @@ package net.corda.flows import com.google.common.util.concurrent.ListenableFuture -import net.corda.testing.contracts.calculateRandomlySizedAmounts +import net.corda.contracts.asset.Cash import net.corda.core.contracts.Amount import net.corda.core.contracts.DOLLARS import net.corda.core.contracts.currency import net.corda.core.flows.FlowException -import net.corda.core.internal.FlowStateMachine import net.corda.core.getOrThrow import net.corda.core.identity.Party -import net.corda.core.map -import net.corda.core.utilities.OpaqueBytes -import net.corda.core.toFuture +import net.corda.core.node.services.Vault +import net.corda.core.node.services.trackBy +import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.transactions.SignedTransaction -import net.corda.testing.DUMMY_NOTARY +import net.corda.core.utilities.OpaqueBytes import net.corda.flows.IssuerFlow.IssuanceRequester -import net.corda.testing.BOC -import net.corda.testing.MEGA_CORP +import net.corda.node.utilities.transaction +import net.corda.testing.* +import net.corda.testing.contracts.calculateRandomlySizedAmounts import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork.MockNode import org.junit.After import org.junit.Before import org.junit.Test -import rx.Observable import java.util.* -import kotlin.test.assertEquals import kotlin.test.assertFailsWith class IssuerFlowTest { @@ -39,11 +37,6 @@ class IssuerFlowTest { notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) bankOfCordaNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name) bankClientNode = mockNet.createPartyNode(notaryNode.network.myAddress, MEGA_CORP.name) - val nodes = listOf(notaryNode, bankOfCordaNode, bankClientNode) - - nodes.forEach { node -> - nodes.map { it.info.legalIdentityAndCert }.forEach(node.services.identityService::registerIdentity) - } } @After @@ -53,24 +46,82 @@ class IssuerFlowTest { @Test fun `test issuer flow`() { - // using default IssueTo Party Reference - val (issuer, issuerResult) = runIssuerAndIssueRequester(bankOfCordaNode, bankClientNode, 1000000.DOLLARS, - bankClientNode.info.legalIdentity, OpaqueBytes.of(123)) - assertEquals(issuerResult.get().stx, issuer.get().resultFuture.get()) + val (vaultUpdatesBoc, vaultUpdatesBankClient) = bankOfCordaNode.database.transaction { + // Register for vault updates + val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL) + val (_, vaultUpdatesBoc) = bankOfCordaNode.services.vaultQueryService.trackBy(criteria) + val (_, vaultUpdatesBankClient) = bankClientNode.services.vaultQueryService.trackBy(criteria) + // using default IssueTo Party Reference + val issuerResult = runIssuerAndIssueRequester(bankOfCordaNode, bankClientNode, 1000000.DOLLARS, + bankClientNode.info.legalIdentity, OpaqueBytes.of(123)) + issuerResult.get() + + Pair(vaultUpdatesBoc, vaultUpdatesBankClient) + } + + // Check Bank of Corda Vault Updates + vaultUpdatesBoc.expectEvents { + sequence( + // ISSUE + expect { update -> + require(update.consumed.isEmpty()) { "Expected 0 consumed states, actual: $update" } + require(update.produced.size == 1) { "Expected 1 produced states, actual: $update" } + val issued = update.produced.single().state.data as Cash.State + require(issued.owner == bankOfCordaNode.info.legalIdentity) + require(issued.owner != bankClientNode.info.legalIdentity) + }, + // MOVE + expect { update -> + require(update.consumed.size == 1) { "Expected 1 consumed states, actual: $update" } + require(update.produced.isEmpty()) { "Expected 0 produced states, actual: $update" } + } + ) + } + + // Check Bank Client Vault Updates + vaultUpdatesBankClient.expectEvents { + // MOVE + expect { update -> + require(update.consumed.isEmpty()) { update.consumed.size } + require(update.produced.size == 1) { update.produced.size } + val paidState = update.produced.single().state.data as Cash.State + require(paidState.owner == bankClientNode.info.legalIdentity) + } + } + } + + @Test + fun `test issuer flow rejects restricted`() { // try to issue an amount of a restricted currency assertFailsWith { runIssuerAndIssueRequester(bankOfCordaNode, bankClientNode, Amount(100000L, currency("BRL")), - bankClientNode.info.legalIdentity, OpaqueBytes.of(123)).issueRequestResult.getOrThrow() + bankClientNode.info.legalIdentity, OpaqueBytes.of(123)).getOrThrow() } } @Test fun `test issue flow to self`() { - // using default IssueTo Party Reference - val (issuer, issuerResult) = runIssuerAndIssueRequester(bankOfCordaNode, bankOfCordaNode, 1000000.DOLLARS, - bankOfCordaNode.info.legalIdentity, OpaqueBytes.of(123)) - assertEquals(issuerResult.get().stx, issuer.get().resultFuture.get()) + val vaultUpdatesBoc = bankOfCordaNode.database.transaction { + val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL) + val (_, vaultUpdatesBoc) = bankOfCordaNode.services.vaultQueryService.trackBy(criteria) + + // using default IssueTo Party Reference + runIssuerAndIssueRequester(bankOfCordaNode, bankOfCordaNode, 1000000.DOLLARS, + bankOfCordaNode.info.legalIdentity, OpaqueBytes.of(123)).getOrThrow() + vaultUpdatesBoc + } + + // Check Bank of Corda Vault Updates + vaultUpdatesBoc.expectEvents { + sequence( + // ISSUE + expect { update -> + require(update.consumed.isEmpty()) { "Expected 0 consumed states, actual: $update" } + require(update.produced.size == 1) { "Expected 1 produced states, actual: $update" } + } + ) + } } @Test @@ -83,7 +134,7 @@ class IssuerFlowTest { bankClientNode.info.legalIdentity, OpaqueBytes.of(123)) } handles.forEach { - require(it.issueRequestResult.get().stx is SignedTransaction) + require(it.get().stx is SignedTransaction) } } @@ -91,20 +142,10 @@ class IssuerFlowTest { issueToNode: MockNode, amount: Amount, party: Party, - ref: OpaqueBytes): RunResult { + ref: OpaqueBytes): ListenableFuture { val issueToPartyAndRef = party.ref(ref) - val issuerFlows: Observable = issuerNode.registerInitiatedFlow(IssuerFlow.Issuer::class.java) - val firstIssuerFiber = issuerFlows.toFuture().map { it.stateMachine } - val issueRequest = IssuanceRequester(amount, party, issueToPartyAndRef.reference, issuerNode.info.legalIdentity, anonymous = false) - val issueRequestResultFuture = issueToNode.services.startFlow(issueRequest).resultFuture - - return IssuerFlowTest.RunResult(firstIssuerFiber, issueRequestResultFuture) + return issueToNode.services.startFlow(issueRequest).resultFuture } - - private data class RunResult( - val issuer: ListenableFuture>, - val issueRequestResult: ListenableFuture - ) } \ No newline at end of file diff --git a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt index ba3ce39b22..8c2ec6d765 100644 --- a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt +++ b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt @@ -1,10 +1,14 @@ package net.corda.bank import com.google.common.util.concurrent.Futures +import net.corda.contracts.asset.Cash import net.corda.core.contracts.DOLLARS import net.corda.core.getOrThrow import net.corda.core.messaging.startFlow import net.corda.core.node.services.ServiceInfo +import net.corda.core.node.services.Vault +import net.corda.core.node.services.trackBy +import net.corda.core.node.services.vault.QueryCriteria import net.corda.flows.IssuerFlow.IssuanceRequester import net.corda.testing.driver.driver import net.corda.node.services.startFlowPermission @@ -33,10 +37,11 @@ class BankOfCordaRPCClientTest { val bigCorpProxy = bigCorpClient.start("bigCorpCFO", "password2").proxy // Register for Bank of Corda Vault updates - val vaultUpdatesBoc = bocProxy.vaultAndUpdates().second + val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL) + val (_, vaultUpdatesBoc) = bocProxy.vaultTrackByCriteria(Cash.State::class.java, criteria) // Register for Big Corporation Vault updates - val vaultUpdatesBigCorp = bigCorpProxy.vaultAndUpdates().second + val (_, vaultUpdatesBigCorp) = bigCorpProxy.vaultTrackByCriteria(Cash.State::class.java, criteria) // Kick-off actual Issuer Flow // TODO: Update checks below to reflect states consumed/produced under anonymisation From 5f7b8f6ec3a912d68fa81df1dabafdb77ac3bb99 Mon Sep 17 00:00:00 2001 From: josecoll Date: Wed, 12 Jul 2017 09:53:15 +0100 Subject: [PATCH 89/97] Vault Query Pagination simplification (#997) * Pagination improvements (fail-fast on too many results without pagination specification) * Fix incorrectly returned results count. * Performance optimisation: only return totalStatesAvailable count on Pagination specification. * Changed DEFAULT_PAGE_NUMBER to 1 (eg. page numbering starts from 1) * Changed MAX_PAGE_SIZE to Int.MAX_VALUE * Fixed compiler WARNINGs in Unit tests. * Fixed minimum page size check (1). * Updated API-RST docs with behavioural notes. * Updated documentation (RST and API); --- .../kotlin/rpc/StandaloneCordaRPClientTest.kt | 7 +- .../net/corda/core/messaging/CordaRPCOps.kt | 19 ++-- .../net/corda/core/node/services/Services.kt | 31 +++--- .../node/services/vault/QueryCriteriaUtils.kt | 18 ++-- docs/source/api-vault-query.rst | 31 +++++- .../vault/HibernateQueryCriteriaParser.kt | 5 + .../services/vault/HibernateVaultQueryImpl.kt | 65 +++++++------ .../services/vault/VaultQueryJavaTests.java | 97 ++++++++----------- .../node/services/vault/VaultQueryTests.kt | 56 ++++++----- 9 files changed, 182 insertions(+), 147 deletions(-) diff --git a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt index a5a6002c50..a01cd27c5b 100644 --- a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt +++ b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt @@ -13,10 +13,7 @@ import net.corda.core.getOrThrow import net.corda.core.messaging.* import net.corda.core.node.NodeInfo import net.corda.core.node.services.Vault -import net.corda.core.node.services.vault.PageSpecification -import net.corda.core.node.services.vault.QueryCriteria -import net.corda.core.node.services.vault.Sort -import net.corda.core.node.services.vault.SortAttribute +import net.corda.core.node.services.vault.* import net.corda.core.seconds import net.corda.core.utilities.OpaqueBytes import net.corda.core.sizedInputStreamAndHash @@ -190,7 +187,7 @@ class StandaloneCordaRPClientTest { .returnValue.getOrThrow(timeout) val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL) - val paging = PageSpecification(0, 10) + val paging = PageSpecification(DEFAULT_PAGE_NUM, 10) val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Standard(Sort.VaultStateAttribute.RECORDED_TIME), Sort.Direction.DESC))) val queryResults = rpcProxy.vaultQueryBy(criteria, paging, sorting) diff --git a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt index 641b1c28e2..abf2e5d4d6 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt @@ -13,6 +13,8 @@ import net.corda.core.identity.Party import net.corda.core.node.NodeInfo import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.Vault +import net.corda.core.node.services.VaultQueryException +import net.corda.core.node.services.vault.DEFAULT_PAGE_SIZE import net.corda.core.node.services.vault.PageSpecification import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.Sort @@ -78,13 +80,18 @@ interface CordaRPCOps : RPCOps { * and returns a [Vault.Page] object containing the following: * 1. states as a List of (page number and size defined by [PageSpecification]) * 2. states metadata as a List of [Vault.StateMetadata] held in the Vault States table. - * 3. the [PageSpecification] used in the query - * 4. a total number of results available (for subsequent paging if necessary) - * 5. status types used in this query: UNCONSUMED, CONSUMED, ALL - * 6. other results (aggregate functions with/without using value groups) + * 3. total number of results available if [PageSpecification] supplied (otherwise returns -1) + * 4. status types used in this query: UNCONSUMED, CONSUMED, ALL + * 5. other results (aggregate functions with/without using value groups) * - * Note: a default [PageSpecification] is applied to the query returning the 1st page (indexed from 0) with up to 200 entries. - * It is the responsibility of the Client to request further pages and/or specify a more suitable [PageSpecification]. + * @throws VaultQueryException if the query cannot be executed for any reason + * (missing criteria or parsing error, paging errors, unsupported query, underlying database error) + * + * Notes + * If no [PageSpecification] is provided, a maximum of [DEFAULT_PAGE_SIZE] results will be returned. + * API users must specify a [PageSpecification] if they are expecting more than [DEFAULT_PAGE_SIZE] results, + * otherwise a [VaultQueryException] will be thrown alerting to this condition. + * It is the responsibility of the API user to request further pages and/or specify a more suitable [PageSpecification]. */ // DOCSTART VaultQueryByAPI @RPCReturnsObservables diff --git a/core/src/main/kotlin/net/corda/core/node/services/Services.kt b/core/src/main/kotlin/net/corda/core/node/services/Services.kt index bdc8f00d0f..e4547d495d 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/Services.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/Services.kt @@ -15,6 +15,7 @@ import net.corda.core.messaging.DataFeed import net.corda.core.node.services.vault.PageSpecification import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.Sort +import net.corda.core.node.services.vault.DEFAULT_PAGE_SIZE import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.OpaqueBytes import net.corda.core.toFuture @@ -118,12 +119,10 @@ class Vault(val states: Iterable>) { * A Page contains: * 1) a [List] of actual [StateAndRef] requested by the specified [QueryCriteria] to a maximum of [MAX_PAGE_SIZE] * 2) a [List] of associated [Vault.StateMetadata], one per [StateAndRef] result - * 3) the [PageSpecification] definition used to bound this result set - * 4) a total number of states that met the given [QueryCriteria] - * Note that this may be more than the specified [PageSpecification.pageSize], and should be used to perform - * further pagination (by issuing new queries). - * 5) Status types used in this query: UNCONSUMED, CONSUMED, ALL - * 6) Other results as a [List] of any type (eg. aggregate function results with/without group by) + * 3) a total number of states that met the given [QueryCriteria] if a [PageSpecification] was provided + * (otherwise defaults to -1) + * 4) Status types used in this query: UNCONSUMED, CONSUMED, ALL + * 5) Other results as a [List] of any type (eg. aggregate function results with/without group by) * * Note: currently otherResults are used only for Aggregate Functions (in which case, the states and statesMetadata * results will be empty) @@ -131,8 +130,7 @@ class Vault(val states: Iterable>) { @CordaSerializable data class Page(val states: List>, val statesMetadata: List, - val pageable: PageSpecification, - val totalStatesAvailable: Int, + val totalStatesAvailable: Long, val stateTypes: StateStatus, val otherResults: List) @@ -353,17 +351,18 @@ interface VaultQueryService { * and returns a [Vault.Page] object containing the following: * 1. states as a List of (page number and size defined by [PageSpecification]) * 2. states metadata as a List of [Vault.StateMetadata] held in the Vault States table. - * 3. the [PageSpecification] used in the query - * 4. a total number of results available (for subsequent paging if necessary) - * 5. status types used in this query: UNCONSUMED, CONSUMED, ALL - * 6. other results (aggregate functions with/without using value groups) + * 3. total number of results available if [PageSpecification] supplied (otherwise returns -1) + * 4. status types used in this query: UNCONSUMED, CONSUMED, ALL + * 5. other results (aggregate functions with/without using value groups) * * @throws VaultQueryException if the query cannot be executed for any reason - * (missing criteria or parsing error, invalid operator, unsupported query, underlying database error) + * (missing criteria or parsing error, paging errors, unsupported query, underlying database error) * - * Note: a default [PageSpecification] is applied to the query returning the 1st page (indexed from 0) with up to 200 entries. - * It is the responsibility of the Client to request further pages and/or specify a more suitable [PageSpecification]. - * Note2: you can also annotate entity fields with JPA OrderBy annotation to achieve the same effect as explicit sorting + * Notes + * If no [PageSpecification] is provided, a maximum of [DEFAULT_PAGE_SIZE] results will be returned. + * API users must specify a [PageSpecification] if they are expecting more than [DEFAULT_PAGE_SIZE] results, + * otherwise a [VaultQueryException] will be thrown alerting to this condition. + * It is the responsibility of the API user to request further pages and/or specify a more suitable [PageSpecification]. */ @Throws(VaultQueryException::class) fun _queryBy(criteria: QueryCriteria, diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt index 6c72d34672..92817ac424 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt @@ -119,21 +119,25 @@ fun getColumnName(column: Column): String { * paging and sorting capability: * https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/repository/PagingAndSortingRepository.html */ -const val DEFAULT_PAGE_NUM = 0 +const val DEFAULT_PAGE_NUM = 1 const val DEFAULT_PAGE_SIZE = 200 /** - * Note: this maximum size will be configurable in future (to allow for large JVM heap sized node configurations) - * Use [PageSpecification] to correctly handle a number of bounded pages of [MAX_PAGE_SIZE]. + * Note: use [PageSpecification] to correctly handle a number of bounded pages of a pre-configured page size. */ -const val MAX_PAGE_SIZE = 512 +const val MAX_PAGE_SIZE = Int.MAX_VALUE /** - * PageSpecification allows specification of a page number (starting from 0 as default) and page size (defaulting to - * [DEFAULT_PAGE_SIZE] with a maximum page size of [MAX_PAGE_SIZE] + * [PageSpecification] allows specification of a page number (starting from [DEFAULT_PAGE_NUM]) and page size + * (defaulting to [DEFAULT_PAGE_SIZE] with a maximum page size of [MAX_PAGE_SIZE]) + * Note: we default the page number to [DEFAULT_PAGE_SIZE] to enable queries without requiring a page specification + * but enabling detection of large results sets that fall out of the [DEFAULT_PAGE_SIZE] requirement. + * [MAX_PAGE_SIZE] should be used with extreme caution as results may exceed your JVM memory footprint. */ @CordaSerializable -data class PageSpecification(val pageNumber: Int = DEFAULT_PAGE_NUM, val pageSize: Int = DEFAULT_PAGE_SIZE) +data class PageSpecification(val pageNumber: Int = -1, val pageSize: Int = DEFAULT_PAGE_SIZE) { + val isDefault = (pageSize == DEFAULT_PAGE_SIZE && pageNumber == -1) +} /** * Sort allows specification of a set of entity attribute names and their associated directionality diff --git a/docs/source/api-vault-query.rst b/docs/source/api-vault-query.rst index fd31eb9497..84683b9763 100644 --- a/docs/source/api-vault-query.rst +++ b/docs/source/api-vault-query.rst @@ -48,7 +48,7 @@ The API provides both static (snapshot) and dynamic (snapshot with streaming upd .. note:: Streaming updates are only filtered based on contract type and state status (UNCONSUMED, CONSUMED, ALL) Simple pagination (page number and size) and sorting (directional ordering using standard or custom property attributes) is also specifiable. -Defaults are defined for Paging (pageNumber = 0, pageSize = 200) and Sorting (direction = ASC). +Defaults are defined for Paging (pageNumber = 1, pageSize = 200) and Sorting (direction = ASC). The ``QueryCriteria`` interface provides a flexible mechanism for specifying different filtering criteria, including and/or composition and a rich set of operators to include: binary logical (AND, OR), comparison (LESS_THAN, LESS_THAN_OR_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL), equality (EQUAL, NOT_EQUAL), likeness (LIKE, NOT_LIKE), nullability (IS_NULL, NOT_NULL), and collection based (IN, NOT_IN). Standard SQL-92 aggregate functions (SUM, AVG, MIN, MAX, COUNT) are also supported. @@ -104,6 +104,15 @@ An example of a custom query in Java is illustrated here: .. note:: Current queries by ``Party`` specify the ``AbstractParty`` which may be concrete or anonymous. In the later case, where an anonymous party does not have an associated X500Name, then no query results will ever be produced. For performance reasons, queries do not use PublicKey as search criteria. Ongoing design work on identity manangement is likely to enhance identity based queries (including composite key criteria selection). +Pagination +---------- +The API provides support for paging where large numbers of results are expected (by default, a page size is set to 200 results). +Defining a sensible default page size enables efficient checkpointing within flows, and frees the developer from worrying about pagination where +result sets are expected to be constrained to 200 or fewer entries. Where large result sets are expected (such as using the RPC API for reporting and/or UI display), it is strongly recommended to define a ``PageSpecification`` to correctly process results with efficient memory utilistion. A fail-fast mode is in place to alert API users to the need for pagination where a single query returns more than 200 results and no ``PageSpecification`` +has been supplied. + +.. note:: A pages maximum size ``MAX_PAGE_SIZE`` is defined as ``Int.MAX_VALUE`` and should be used with extreme caution as results returned may exceed your JVM's memory footprint. + Example usage ------------- @@ -284,7 +293,7 @@ Track unconsumed linear states: :end-before: DOCEND VaultQueryExample16 .. note:: This will return both Deal and Linear states. - + Track unconsumed deal states: .. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -369,6 +378,17 @@ Track unconsumed deal states or linear states (with snapshot including specifica :start-after: DOCSTART VaultJavaQueryExample4 :end-before: DOCEND VaultJavaQueryExample4 +Behavioural notes +----------------- +1. **TrackBy** updates do not take into account the full criteria specification due to different and more restrictive syntax + in `observables `_ filtering (vs full SQL-92 JDBC filtering as used in snapshot views). + Specifically, dynamic updates are filtered by ``contractType`` and ``stateType`` (UNCONSUMED, CONSUMED, ALL) only. +2. **QueryBy** and **TrackBy snapshot views** using pagination may return different result sets as each paging request is a + separate SQL query on the underlying database, and it is entirely conceivable that state modifications are taking + place in between and/or in parallel to paging requests. + When using pagination, always check the value of the ``totalStatesAvailable`` (from the ``Vault.Page`` result) and + adjust further paging requests appropriately. + Other use case scenarios ------------------------ @@ -410,10 +430,11 @@ This query returned an ``Iterable>`` The query returns a ``Vault.Page`` result containing: - - states as a ``List>`` sized according to the default Page specification of ``DEFAULT_PAGE_NUM`` (0) and ``DEFAULT_PAGE_SIZE`` (200). + - states as a ``List>`` up to a maximum of ``DEFAULT_PAGE_SIZE`` (200) where no ``PageSpecification`` provided, otherwise returns results according to the parameters ``pageNumber`` and ``pageSize`` specified in the supplied ``PageSpecification``. - states metadata as a ``List`` containing Vault State metadata held in the Vault states table. - - the ``PagingSpecification`` used in the query - - a ``total`` number of results available. This value can be used issue subsequent queries with appropriately specified ``PageSpecification`` (according to your paging needs and/or maximum memory capacity for holding large data sets). Note it is your responsibility to manage page numbers and sizes. + - a ``total`` number of results available if ``PageSpecification`` provided (otherwise returns -1). For pagination, this value can be used to issue subsequent queries with appropriately specified ``PageSpecification`` parameters (according to your paging needs and/or maximum memory capacity for holding large data sets). Note it is your responsibility to manage page numbers and sizes. + - status types used in this query: UNCONSUMED, CONSUMED, ALL + - other results as a [List] of any type (eg. aggregate function results with/without group by) 2. ServiceHub usage obtaining linear heads for a given contract state type diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt index 8a55057ac3..9cc3fad23d 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt @@ -95,6 +95,7 @@ class HibernateQueryCriteriaParser(val contractType: Class, } is ColumnPredicate.BinaryComparison -> { column as Path?> + @Suppress("UNCHECKED_CAST") val literal = columnPredicate.rightLiteral as Comparable? when (columnPredicate.operator) { BinaryComparisonOperator.GREATER_THAN -> criteriaBuilder.greaterThan(column, literal) @@ -117,8 +118,11 @@ class HibernateQueryCriteriaParser(val contractType: Class, } } is ColumnPredicate.Between -> { + @Suppress("UNCHECKED_CAST") column as Path?> + @Suppress("UNCHECKED_CAST") val fromLiteral = columnPredicate.rightFromLiteral as Comparable? + @Suppress("UNCHECKED_CAST") val toLiteral = columnPredicate.rightToLiteral as Comparable? criteriaBuilder.between(column, fromLiteral, toLiteral) } @@ -164,6 +168,7 @@ class HibernateQueryCriteriaParser(val contractType: Class, val columnPredicate = expression.predicate when (columnPredicate) { is ColumnPredicate.AggregateFunction -> { + @Suppress("UNCHECKED_CAST") column as Path? val aggregateExpression = when (columnPredicate.type) { diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt index bb3c5d8e88..2d10eaaf63 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt @@ -11,10 +11,8 @@ import net.corda.core.messaging.DataFeed import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultQueryException import net.corda.core.node.services.VaultQueryService -import net.corda.core.node.services.vault.MAX_PAGE_SIZE -import net.corda.core.node.services.vault.PageSpecification -import net.corda.core.node.services.vault.QueryCriteria -import net.corda.core.node.services.vault.Sort +import net.corda.core.node.services.vault.* +import net.corda.core.node.services.vault.QueryCriteria.VaultCustomQueryCriteria import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.deserialize import net.corda.core.serialization.storageKryo @@ -43,6 +41,15 @@ class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration, override fun _queryBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractType: Class): Vault.Page { log.info("Vault Query for contract type: $contractType, criteria: $criteria, pagination: $paging, sorting: $sorting") + // calculate total results where a page specification has been defined + var totalStates = -1L + if (!paging.isDefault) { + val count = builder { VaultSchemaV1.VaultStates::recordedTime.count() } + val countCriteria = VaultCustomQueryCriteria(count) + val results = queryBy(contractType, criteria.and(countCriteria)) + totalStates = results.otherResults[0] as Long + } + val session = sessionFactory.withOptions(). connection(TransactionManager.current().connection). openSession() @@ -62,43 +69,45 @@ class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration, // prepare query for execution val query = session.createQuery(criteriaQuery) - // pagination - if (paging.pageNumber < 0) throw VaultQueryException("Page specification: invalid page number ${paging.pageNumber} [page numbers start from 0]") - if (paging.pageSize < 0 || paging.pageSize > MAX_PAGE_SIZE) throw VaultQueryException("Page specification: invalid page size ${paging.pageSize} [maximum page size is ${MAX_PAGE_SIZE}]") + // pagination checks + if (!paging.isDefault) { + // pagination + if (paging.pageNumber < DEFAULT_PAGE_NUM) throw VaultQueryException("Page specification: invalid page number ${paging.pageNumber} [page numbers start from $DEFAULT_PAGE_NUM]") + if (paging.pageSize < 1) throw VaultQueryException("Page specification: invalid page size ${paging.pageSize} [must be a value between 1 and $MAX_PAGE_SIZE]") + } - // count total results available - val countQuery = criteriaBuilder.createQuery(Long::class.java) - countQuery.select(criteriaBuilder.count(countQuery.from(VaultSchemaV1.VaultStates::class.java))) - val totalStates = session.createQuery(countQuery).singleResult.toInt() - - if ((paging.pageNumber != 0) && (paging.pageSize * paging.pageNumber >= totalStates)) - throw VaultQueryException("Requested more results than available [${paging.pageSize} * ${paging.pageNumber} >= ${totalStates}]") - - query.firstResult = paging.pageNumber * paging.pageSize - query.maxResults = paging.pageSize + query.firstResult = (paging.pageNumber - 1) * paging.pageSize + query.maxResults = paging.pageSize + 1 // detection too many results // execution val results = query.resultList - val statesAndRefs: MutableList> = mutableListOf() + + // final pagination check (fail-fast on too many results when no pagination specified) + if (paging.isDefault && results.size > DEFAULT_PAGE_SIZE) + throw VaultQueryException("Please specify a `PageSpecification` as there are more results [${results.size}] than the default page size [$DEFAULT_PAGE_SIZE]") + + val statesAndRefs: MutableList> = mutableListOf() val statesMeta: MutableList = mutableListOf() val otherResults: MutableList = mutableListOf() results.asSequence() - .forEach { it -> - if (it[0] is VaultSchemaV1.VaultStates) { - val it = it[0] as VaultSchemaV1.VaultStates - val stateRef = StateRef(SecureHash.parse(it.stateRef!!.txId!!), it.stateRef!!.index!!) - val state = it.contractState.deserialize>(storageKryo()) - statesMeta.add(Vault.StateMetadata(stateRef, it.contractStateClassName, it.recordedTime, it.consumedTime, it.stateStatus, it.notaryName, it.notaryKey, it.lockId, it.lockUpdateTime)) + .forEachIndexed { index, result -> + if (result[0] is VaultSchemaV1.VaultStates) { + if (!paging.isDefault && index == paging.pageSize) // skip last result if paged + return@forEachIndexed + val vaultState = result[0] as VaultSchemaV1.VaultStates + val stateRef = StateRef(SecureHash.parse(vaultState.stateRef!!.txId!!), vaultState.stateRef!!.index!!) + val state = vaultState.contractState.deserialize>(storageKryo()) + statesMeta.add(Vault.StateMetadata(stateRef, vaultState.contractStateClassName, vaultState.recordedTime, vaultState.consumedTime, vaultState.stateStatus, vaultState.notaryName, vaultState.notaryKey, vaultState.lockId, vaultState.lockUpdateTime)) statesAndRefs.add(StateAndRef(state, stateRef)) } else { - log.debug { "OtherResults: ${Arrays.toString(it.toArray())}" } - otherResults.addAll(it.toArray().asList()) + log.debug { "OtherResults: ${Arrays.toString(result.toArray())}" } + otherResults.addAll(result.toArray().asList()) } } - return Vault.Page(states = statesAndRefs, statesMetadata = statesMeta, pageable = paging, stateTypes = criteriaParser.stateTypes, totalStatesAvailable = totalStates, otherResults = otherResults) as Vault.Page + return Vault.Page(states = statesAndRefs, statesMetadata = statesMeta, stateTypes = criteriaParser.stateTypes, totalStatesAvailable = totalStates, otherResults = otherResults) } catch (e: Exception) { log.error(e.message) @@ -132,6 +141,7 @@ class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration, val contractInterfaceToConcreteTypes = mutableMapOf>() distinctTypes.forEach { it -> + @Suppress("UNCHECKED_CAST") val concreteType = Class.forName(it) as Class val contractInterfaces = deriveContractInterfaces(concreteType) contractInterfaces.map { @@ -146,6 +156,7 @@ class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration, val myInterfaces: MutableSet> = mutableSetOf() clazz.interfaces.forEach { if (!it.equals(ContractState::class.java)) { + @Suppress("UNCHECKED_CAST") myInterfaces.add(it as Class) myInterfaces.addAll(deriveContractInterfaces(it)) } diff --git a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java index c1f58249e1..512a0ab5d3 100644 --- a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java +++ b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java @@ -1,62 +1,45 @@ package net.corda.node.services.vault; -import com.google.common.collect.ImmutableSet; -import kotlin.Pair; -import net.corda.contracts.DealState; -import net.corda.contracts.asset.Cash; +import com.google.common.collect.*; +import kotlin.*; +import net.corda.contracts.*; +import net.corda.contracts.asset.*; import net.corda.core.contracts.*; -import net.corda.testing.contracts.DummyLinearContract; import net.corda.core.crypto.*; -import net.corda.core.identity.AbstractParty; -import net.corda.core.messaging.DataFeed; -import net.corda.core.node.services.Vault; -import net.corda.core.node.services.VaultQueryException; -import net.corda.core.node.services.VaultQueryService; -import net.corda.core.node.services.VaultService; +import net.corda.core.identity.*; +import net.corda.core.messaging.*; +import net.corda.core.node.services.*; import net.corda.core.node.services.vault.*; -import net.corda.core.node.services.vault.QueryCriteria.LinearStateQueryCriteria; -import net.corda.core.node.services.vault.QueryCriteria.VaultCustomQueryCriteria; -import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria; -import net.corda.core.schemas.MappedSchema; -import net.corda.core.schemas.testing.DummyLinearStateSchemaV1; -import net.corda.core.utilities.OpaqueBytes; -import net.corda.core.transactions.SignedTransaction; -import net.corda.core.transactions.WireTransaction; -import net.corda.node.services.database.HibernateConfiguration; -import net.corda.node.services.schema.NodeSchemaService; -import net.corda.schemas.CashSchemaV1; -import net.corda.testing.TestConstants; -import net.corda.testing.contracts.VaultFiller; -import net.corda.testing.node.MockServices; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.exposed.sql.Database; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; +import net.corda.core.node.services.vault.QueryCriteria.*; +import net.corda.core.schemas.*; +import net.corda.core.schemas.testing.*; +import net.corda.core.transactions.*; +import net.corda.core.utilities.*; +import net.corda.node.services.database.*; +import net.corda.node.services.schema.*; +import net.corda.schemas.*; +import net.corda.testing.*; +import net.corda.testing.contracts.*; +import net.corda.testing.node.*; +import org.jetbrains.annotations.*; +import org.jetbrains.exposed.sql.*; +import org.junit.*; import rx.Observable; -import java.io.Closeable; -import java.io.IOException; -import java.lang.reflect.Field; +import java.io.*; +import java.lang.reflect.*; import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; +import java.util.stream.*; -import static net.corda.contracts.asset.CashKt.getDUMMY_CASH_ISSUER; -import static net.corda.contracts.asset.CashKt.getDUMMY_CASH_ISSUER_KEY; -import static net.corda.testing.CoreTestUtils.getBOC; -import static net.corda.testing.CoreTestUtils.getBOC_KEY; -import static net.corda.testing.CoreTestUtils.getBOC_PUBKEY; -import static net.corda.core.contracts.ContractsDSL.USD; -import static net.corda.core.node.services.vault.QueryCriteriaUtils.MAX_PAGE_SIZE; -import static net.corda.node.utilities.DatabaseSupportKt.configureDatabase; +import static net.corda.contracts.asset.CashKt.*; +import static net.corda.core.contracts.ContractsDSL.*; +import static net.corda.core.node.services.vault.QueryCriteriaUtils.*; +import static net.corda.node.utilities.DatabaseSupportKt.*; import static net.corda.node.utilities.DatabaseSupportKt.transaction; -import static net.corda.testing.CoreTestUtils.getMEGA_CORP; -import static net.corda.testing.CoreTestUtils.getMEGA_CORP_KEY; -import static net.corda.testing.node.MockServicesKt.makeTestDataSourceProperties; +import static net.corda.testing.CoreTestUtils.*; +import static net.corda.testing.node.MockServicesKt.*; import static net.corda.core.utilities.ByteArrays.toHexString; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.*; public class VaultQueryJavaTests { @@ -146,7 +129,7 @@ public class VaultQueryJavaTests { List stateRefs = stateRefsStream.collect(Collectors.toList()); SortAttribute.Standard sortAttribute = new SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_TXN_ID); - Sort sorting = new Sort(Arrays.asList(new Sort.SortColumn(sortAttribute, Sort.Direction.ASC))); + Sort sorting = new Sort(Collections.singletonList(new Sort.SortColumn(sortAttribute, Sort.Direction.ASC))); VaultQueryCriteria criteria = new VaultQueryCriteria(Vault.StateStatus.UNCONSUMED, null, stateRefs); Vault.Page results = vaultQuerySvc.queryBy(DummyLinearContract.State.class, criteria, sorting); @@ -219,7 +202,7 @@ public class VaultQueryJavaTests { QueryCriteria compositeCriteria1 = dealCriteriaAll.or(linearCriteriaAll); QueryCriteria compositeCriteria2 = vaultCriteria.and(compositeCriteria1); - PageSpecification pageSpec = new PageSpecification(0, MAX_PAGE_SIZE); + PageSpecification pageSpec = new PageSpecification(DEFAULT_PAGE_NUM, MAX_PAGE_SIZE); Sort.SortColumn sortByUid = new Sort.SortColumn(new SortAttribute.Standard(Sort.LinearStateAttribute.UUID), Sort.Direction.DESC); Sort sorting = new Sort(ImmutableSet.of(sortByUid)); Vault.Page results = vaultQuerySvc.queryBy(LinearState.class, compositeCriteria2, pageSpec, sorting); @@ -232,6 +215,7 @@ public class VaultQueryJavaTests { } @Test + @SuppressWarnings("unchecked") public void customQueryForCashStatesWithAmountOfCurrencyGreaterOrEqualThanQuantity() { transaction(database, tx -> { @@ -328,7 +312,7 @@ public class VaultQueryJavaTests { QueryCriteria dealOrLinearIdCriteria = dealCriteria.or(linearCriteria); QueryCriteria compositeCriteria = dealOrLinearIdCriteria.and(vaultCriteria); - PageSpecification pageSpec = new PageSpecification(0, MAX_PAGE_SIZE); + PageSpecification pageSpec = new PageSpecification(DEFAULT_PAGE_NUM, MAX_PAGE_SIZE); Sort.SortColumn sortByUid = new Sort.SortColumn(new SortAttribute.Standard(Sort.LinearStateAttribute.UUID), Sort.Direction.DESC); Sort sorting = new Sort(ImmutableSet.of(sortByUid)); DataFeed, Vault.Update> results = vaultQuerySvc.trackBy(ContractState.class, compositeCriteria, pageSpec, sorting); @@ -408,6 +392,7 @@ public class VaultQueryJavaTests { */ @Test + @SuppressWarnings("unchecked") public void aggregateFunctionsWithoutGroupClause() { transaction(database, tx -> { @@ -452,6 +437,7 @@ public class VaultQueryJavaTests { } @Test + @SuppressWarnings("unchecked") public void aggregateFunctionsWithSingleGroupClause() { transaction(database, tx -> { @@ -472,11 +458,11 @@ public class VaultQueryJavaTests { Field pennies = CashSchemaV1.PersistentCashState.class.getDeclaredField("pennies"); Field currency = CashSchemaV1.PersistentCashState.class.getDeclaredField("currency"); - QueryCriteria sumCriteria = new VaultCustomQueryCriteria(Builder.sum(pennies, Arrays.asList(currency))); + QueryCriteria sumCriteria = new VaultCustomQueryCriteria(Builder.sum(pennies, Collections.singletonList(currency))); QueryCriteria countCriteria = new VaultCustomQueryCriteria(Builder.count(pennies)); - QueryCriteria maxCriteria = new VaultCustomQueryCriteria(Builder.max(pennies, Arrays.asList(currency))); - QueryCriteria minCriteria = new VaultCustomQueryCriteria(Builder.min(pennies, Arrays.asList(currency))); - QueryCriteria avgCriteria = new VaultCustomQueryCriteria(Builder.avg(pennies, Arrays.asList(currency))); + QueryCriteria maxCriteria = new VaultCustomQueryCriteria(Builder.max(pennies, Collections.singletonList(currency))); + QueryCriteria minCriteria = new VaultCustomQueryCriteria(Builder.min(pennies, Collections.singletonList(currency))); + QueryCriteria avgCriteria = new VaultCustomQueryCriteria(Builder.avg(pennies, Collections.singletonList(currency))); QueryCriteria criteria = sumCriteria.and(countCriteria).and(maxCriteria).and(minCriteria).and(avgCriteria); Vault.Page results = vaultQuerySvc.queryBy(Cash.State.class, criteria); @@ -522,6 +508,7 @@ public class VaultQueryJavaTests { } @Test + @SuppressWarnings("unchecked") public void aggregateFunctionsSumByIssuerAndCurrencyAndSortByAggregateSum() { transaction(database, tx -> { diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index 3e27f42042..997da4b345 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -37,10 +37,8 @@ import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.bouncycastle.asn1.x500.X500Name import org.jetbrains.exposed.sql.Database -import org.junit.After -import org.junit.Before -import org.junit.Ignore -import org.junit.Test +import org.junit.* +import org.junit.rules.ExpectedException import java.io.Closeable import java.lang.Thread.sleep import java.math.BigInteger @@ -825,7 +823,7 @@ class VaultQueryTests { // Last page implies we need to perform a row count for the Query first, // and then re-query for a given offset defined by (count - pageSize) - val pagingSpec = PageSpecification(9, 10) + val pagingSpec = PageSpecification(10, 10) val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) val results = vaultQuerySvc.queryBy(criteria, paging = pagingSpec) @@ -834,48 +832,54 @@ class VaultQueryTests { } } + @get:Rule + val expectedEx = ExpectedException.none()!! + // pagination: invalid page number - @Test(expected = VaultQueryException::class) + @Test fun `invalid page number`() { + expectedEx.expect(VaultQueryException::class.java) + expectedEx.expectMessage("Page specification: invalid page number") + database.transaction { services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 100, 100, Random(0L)) - val pagingSpec = PageSpecification(-1, 10) + val pagingSpec = PageSpecification(0, 10) val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) - val results = vaultQuerySvc.queryBy(criteria, paging = pagingSpec) - assertThat(results.states).hasSize(10) // should retrieve states 90..99 + vaultQuerySvc.queryBy(criteria, paging = pagingSpec) } } // pagination: invalid page size - @Test(expected = VaultQueryException::class) + @Test fun `invalid page size`() { + expectedEx.expect(VaultQueryException::class.java) + expectedEx.expectMessage("Page specification: invalid page size") + database.transaction { services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 100, 100, Random(0L)) - val pagingSpec = PageSpecification(0, MAX_PAGE_SIZE + 1) - + val pagingSpec = PageSpecification(DEFAULT_PAGE_NUM, MAX_PAGE_SIZE + 1) // overflow = -2147483648 val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) vaultQuerySvc.queryBy(criteria, paging = pagingSpec) - assertFails { } } } - // pagination: out or range request (page number * page size) > total rows available - @Test(expected = VaultQueryException::class) - fun `out of range page request`() { + // pagination not specified but more than DEFAULT_PAGE_SIZE results available (fail-fast test) + @Test + fun `pagination not specified but more than default results available`() { + expectedEx.expect(VaultQueryException::class.java) + expectedEx.expectMessage("Please specify a `PageSpecification`") + database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 100, 100, Random(0L)) - - val pagingSpec = PageSpecification(10, 10) // this requests results 101 .. 110 + services.fillWithSomeTestCash(201.DOLLARS, DUMMY_NOTARY, 201, 201, Random(0L)) val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) - val results = vaultQuerySvc.queryBy(criteria, paging = pagingSpec) - assertFails { println("Query should throw an exception [${results.states.count()}]") } + vaultQuerySvc.queryBy(criteria) } } @@ -1776,7 +1780,7 @@ class VaultQueryTests { updates } - updates?.expectEvents { + updates.expectEvents { sequence( expect { (consumed, produced, flowId) -> require(flowId == null) {} @@ -1823,7 +1827,7 @@ class VaultQueryTests { updates } - updates?.expectEvents { + updates.expectEvents { sequence( expect { (consumed, produced, flowId) -> require(flowId == null) {} @@ -1870,7 +1874,7 @@ class VaultQueryTests { updates } - updates?.expectEvents { + updates.expectEvents { sequence( expect { (consumed, produced, flowId) -> require(flowId == null) {} @@ -1926,7 +1930,7 @@ class VaultQueryTests { updates } - updates?.expectEvents { + updates.expectEvents { sequence( expect { (consumed, produced, flowId) -> require(flowId == null) {} @@ -1976,7 +1980,7 @@ class VaultQueryTests { updates } - updates?.expectEvents { + updates.expectEvents { sequence( expect { (consumed, produced, flowId) -> require(flowId == null) {} From 78ecff7933052c6749b1a2bfd504cb0d4adfd28a Mon Sep 17 00:00:00 2001 From: Patrick Kuo Date: Wed, 12 Jul 2017 12:13:29 +0100 Subject: [PATCH 90/97] Added composite key provider for storing composite keys in keystore (#1006) * Add unit tests around decoding composite keys (cherry picked from commit 9ccdd8e) * Start writing a Composite signature scheme (cherry picked from commit 72ac3a5) * Composite key serialisation * refactoring * * Address PR issues * * Address PR issues * * Address PR issues * * Address PR issues * fix up after rebase --- .../net/corda/jackson/JacksonSupport.kt | 1 + .../kotlin/net/corda/core/crypto/Crypto.kt | 30 +++++- .../net/corda/core/crypto/CryptoUtils.kt | 1 + .../net/corda/core/crypto/SignatureScheme.kt | 2 +- .../crypto/{ => composite}/CompositeKey.kt | 85 +++++++++++------ .../{ => composite}/CompositeSignature.kt | 12 +-- .../CompositeSignaturesWithKeys.kt | 3 +- .../corda/core/crypto/composite/KeyFactory.kt | 34 +++++++ .../crypto/provider/CordaSecurityProvider.kt | 37 ++++++++ .../net/corda/core/node/services/Services.kt | 2 +- .../serialization/DefaultKryoCustomizer.kt | 2 +- .../net/corda/core/serialization/Kryo.kt | 1 + .../corda/core/contracts/TransactionTests.kt | 2 +- .../corda/core/crypto/CompositeKeyTests.kt | 95 ++++++++++++++++++- .../net/corda/core/crypto/CryptoUtilsTest.kt | 2 +- .../services/vault/schemas/VaultSchemaTest.kt | 2 +- .../node/services/BFTNotaryServiceTests.kt | 4 +- .../net/corda/node/internal/AbstractNode.kt | 1 + .../utilities/ServiceIdentityGenerator.kt | 3 +- .../main/kotlin/net/corda/testing/TestDSL.kt | 1 + 20 files changed, 269 insertions(+), 51 deletions(-) rename core/src/main/kotlin/net/corda/core/crypto/{ => composite}/CompositeKey.kt (76%) rename core/src/main/kotlin/net/corda/core/crypto/{ => composite}/CompositeSignature.kt (87%) rename core/src/main/kotlin/net/corda/core/crypto/{ => composite}/CompositeSignaturesWithKeys.kt (83%) create mode 100644 core/src/main/kotlin/net/corda/core/crypto/composite/KeyFactory.kt create mode 100644 core/src/main/kotlin/net/corda/core/crypto/provider/CordaSecurityProvider.kt diff --git a/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt b/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt index 0d72c0d3d5..1ebb881d19 100644 --- a/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt +++ b/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt @@ -10,6 +10,7 @@ import com.fasterxml.jackson.module.kotlin.KotlinModule import net.corda.contracts.BusinessCalendar import net.corda.core.contracts.Amount import net.corda.core.crypto.* +import net.corda.core.crypto.composite.CompositeKey import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party diff --git a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt index a92ab97845..abfcaa964e 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt @@ -1,6 +1,9 @@ package net.corda.core.crypto -import net.corda.core.crypto.random63BitValue +import net.corda.core.crypto.composite.CompositeKey +import net.corda.core.crypto.composite.CompositeSignature +import net.corda.core.crypto.provider.CordaObjectIdentifier +import net.corda.core.crypto.provider.CordaSecurityProvider import net.i2p.crypto.eddsa.EdDSAEngine import net.i2p.crypto.eddsa.EdDSAPrivateKey import net.i2p.crypto.eddsa.EdDSAPublicKey @@ -147,6 +150,22 @@ object Crypto { "at the cost of larger key sizes and loss of compatibility." ) + /** + * Corda composite key type + */ + val COMPOSITE_KEY = SignatureScheme( + 6, + "COMPOSITE", + AlgorithmIdentifier(CordaObjectIdentifier.compositeKey), + emptyList(), + CordaSecurityProvider.PROVIDER_NAME, + CompositeKey.KEY_ALGORITHM, + CompositeSignature.SIGNATURE_ALGORITHM, + null, + null, + "Composite keys composed from individual public keys" + ) + /** Our default signature scheme if no algorithm is specified (e.g. for key generation). */ val DEFAULT_SIGNATURE_SCHEME = EDDSA_ED25519_SHA512 @@ -159,7 +178,8 @@ object Crypto { ECDSA_SECP256K1_SHA256, ECDSA_SECP256R1_SHA256, EDDSA_ED25519_SHA512, - SPHINCS256_SHA256 + SPHINCS256_SHA256, + COMPOSITE_KEY ).associateBy { it.schemeCodeName } /** @@ -177,6 +197,7 @@ object Crypto { // The val is private to avoid any harmful state changes. private val providerMap: Map = mapOf( BouncyCastleProvider.PROVIDER_NAME to getBouncyCastleProvider(), + CordaSecurityProvider.PROVIDER_NAME to CordaSecurityProvider(), "BCPQC" to BouncyCastlePQCProvider()) // unfortunately, provider's name is not final in BouncyCastlePQCProvider, so we explicitly set it. private fun getBouncyCastleProvider() = BouncyCastleProvider().apply { @@ -188,6 +209,7 @@ object Crypto { // This registration is needed for reading back EdDSA key from java keystore. // TODO: Find a way to make JKS work with bouncy castle provider or implement our own provide so we don't have to register bouncy castle provider. Security.addProvider(getBouncyCastleProvider()) + Security.addProvider(CordaSecurityProvider()) } /** @@ -202,7 +224,7 @@ object Crypto { } fun findSignatureScheme(algorithm: AlgorithmIdentifier): SignatureScheme { - return algorithmMap[normaliseAlgorithmIdentifier(algorithm)] ?: throw IllegalArgumentException("Unrecognised algorithm: ${algorithm}") + return algorithmMap[normaliseAlgorithmIdentifier(algorithm)] ?: throw IllegalArgumentException("Unrecognised algorithm: ${algorithm.algorithm.id}") } /** @@ -543,7 +565,7 @@ object Crypto { if (signatureScheme.algSpec != null) keyPairGenerator.initialize(signatureScheme.algSpec, newSecureRandom()) else - keyPairGenerator.initialize(signatureScheme.keySize, newSecureRandom()) + keyPairGenerator.initialize(signatureScheme.keySize!!, newSecureRandom()) return keyPairGenerator.generateKeyPair() } diff --git a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt index 310f34ed7f..4bd951edaf 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt @@ -2,6 +2,7 @@ package net.corda.core.crypto +import net.corda.core.crypto.composite.CompositeKey import net.corda.core.identity.Party import net.corda.core.utilities.OpaqueBytes import java.math.BigInteger diff --git a/core/src/main/kotlin/net/corda/core/crypto/SignatureScheme.kt b/core/src/main/kotlin/net/corda/core/crypto/SignatureScheme.kt index c6f4c7a9ab..49493f6d6f 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/SignatureScheme.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/SignatureScheme.kt @@ -28,5 +28,5 @@ data class SignatureScheme( val algorithmName: String, val signatureName: String, val algSpec: AlgorithmParameterSpec?, - val keySize: Int, + val keySize: Int?, val desc: String) diff --git a/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt b/core/src/main/kotlin/net/corda/core/crypto/composite/CompositeKey.kt similarity index 76% rename from core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt rename to core/src/main/kotlin/net/corda/core/crypto/composite/CompositeKey.kt index 75bdb2e73b..51a13a076a 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/composite/CompositeKey.kt @@ -1,8 +1,14 @@ -package net.corda.core.crypto +package net.corda.core.crypto.composite -import net.corda.core.crypto.CompositeKey.NodeAndWeight +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.composite.CompositeKey.NodeAndWeight +import net.corda.core.crypto.keys +import net.corda.core.crypto.provider.CordaObjectIdentifier +import net.corda.core.crypto.toSHA256Bytes +import net.corda.core.crypto.toStringShort import net.corda.core.serialization.CordaSerializable import org.bouncycastle.asn1.* +import org.bouncycastle.asn1.x509.AlgorithmIdentifier import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo import java.nio.ByteBuffer import java.security.PublicKey @@ -26,9 +32,35 @@ import java.util.* * signatures required) to satisfy the sub-tree rooted at this node. */ @CordaSerializable -class CompositeKey private constructor (val threshold: Int, - children: List) : PublicKey { +class CompositeKey private constructor(val threshold: Int, children: List) : PublicKey { + companion object { + val KEY_ALGORITHM = "COMPOSITE" + /** + * Build a composite key from a DER encoded form. + */ + fun getInstance(encoded: ByteArray) = getInstance(ASN1Primitive.fromByteArray(encoded)) + + fun getInstance(asn1: ASN1Primitive): PublicKey { + val keyInfo = SubjectPublicKeyInfo.getInstance(asn1) + require(keyInfo.algorithm.algorithm == CordaObjectIdentifier.compositeKey) + val sequence = ASN1Sequence.getInstance(keyInfo.parsePublicKey()) + val threshold = ASN1Integer.getInstance(sequence.getObjectAt(0)).positiveValue.toInt() + val sequenceOfChildren = ASN1Sequence.getInstance(sequence.getObjectAt(1)) + val builder = Builder() + val listOfChildren = sequenceOfChildren.objects.toList() + listOfChildren.forEach { childAsn1 -> + require(childAsn1 is ASN1Sequence) + val childSeq = childAsn1 as ASN1Sequence + val key = Crypto.decodePublicKey((childSeq.getObjectAt(0) as DERBitString).bytes) + val weight = ASN1Integer.getInstance(childSeq.getObjectAt(1)) + builder.addKey(key, weight.positiveValue.toInt()) + } + return builder.build(threshold) + } + } + val children = children.sorted() + init { // TODO: replace with the more extensive, but slower, checkValidity() test. checkConstraints() @@ -47,8 +79,9 @@ class CompositeKey private constructor (val threshold: Int, require(threshold > 0) { "CompositeKey threshold is set to $threshold, but it should be a positive integer." } // If threshold is bigger than total weight, then it will never be satisfied. val totalWeight = totalWeight() - require(threshold <= totalWeight) { "CompositeKey threshold: $threshold cannot be bigger than aggregated weight of " + - "child nodes: $totalWeight"} + require(threshold <= totalWeight) { + "CompositeKey threshold: $threshold cannot be bigger than aggregated weight of child nodes: $totalWeight" + } } // Graph cycle detection in the composite key structure to avoid infinite loops on CompositeKey graph traversal and @@ -75,7 +108,7 @@ class CompositeKey private constructor (val threshold: Int, * TODO: Always call this method when deserialising [CompositeKey]s. */ fun checkValidity() { - val visitedMap = IdentityHashMap() + val visitedMap = IdentityHashMap() visitedMap.put(this, true) cycleDetection(visitedMap) // Graph cycle testing on the root node. checkConstraints() @@ -93,7 +126,7 @@ class CompositeKey private constructor (val threshold: Int, private fun totalWeight(): Int { var sum = 0 for ((_, weight) in children) { - require (weight > 0) { "Non-positive weight: $weight detected." } + require(weight > 0) { "Non-positive weight: $weight detected." } sum = Math.addExact(sum, weight) // Add and check for integer overflow. } return sum @@ -104,17 +137,17 @@ class CompositeKey private constructor (val threshold: Int, * Each node should be assigned with a positive weight to avoid certain types of weight underflow attacks. */ @CordaSerializable - data class NodeAndWeight(val node: PublicKey, val weight: Int): Comparable, ASN1Object() { - + data class NodeAndWeight(val node: PublicKey, val weight: Int) : Comparable, ASN1Object() { init { // We don't allow zero or negative weights. Minimum weight = 1. - require (weight > 0) { "A non-positive weight was detected. Node info: $this" } + require(weight > 0) { "A non-positive weight was detected. Node info: $this" } } override fun compareTo(other: NodeAndWeight): Int { - return if (weight == other.weight) { + return if (weight == other.weight) ByteBuffer.wrap(node.toSHA256Bytes()).compareTo(ByteBuffer.wrap(other.node.toSHA256Bytes())) - } else weight.compareTo(other.weight) + else + weight.compareTo(other.weight) } override fun toASN1Primitive(): ASN1Primitive { @@ -129,16 +162,13 @@ class CompositeKey private constructor (val threshold: Int, } } - companion object { - val ALGORITHM = CompositeSignature.ALGORITHM_IDENTIFIER.algorithm.toString() - } - /** * Takes single PublicKey and checks if CompositeKey requirements hold for that key. */ fun isFulfilledBy(key: PublicKey) = isFulfilledBy(setOf(key)) - override fun getAlgorithm() = ALGORITHM + override fun getAlgorithm() = KEY_ALGORITHM + override fun getEncoded(): ByteArray { val keyVector = ASN1EncodableVector() val childrenVector = ASN1EncodableVector() @@ -147,13 +177,14 @@ class CompositeKey private constructor (val threshold: Int, } keyVector.add(ASN1Integer(threshold.toLong())) keyVector.add(DERSequence(childrenVector)) - return SubjectPublicKeyInfo(CompositeSignature.ALGORITHM_IDENTIFIER, DERSequence(keyVector)).encoded + return SubjectPublicKeyInfo(AlgorithmIdentifier(CordaObjectIdentifier.compositeKey), DERSequence(keyVector)).encoded } + override fun getFormat() = ASN1Encoding.DER // Extracted method from isFulfilledBy. private fun checkFulfilledBy(keysToCheck: Iterable): Boolean { - if (keysToCheck.any { it is CompositeKey } ) return false + if (keysToCheck.any { it is CompositeKey }) return false val totalWeight = children.map { (node, weight) -> if (node is CompositeKey) { if (node.checkFulfilledBy(keysToCheck)) weight else 0 @@ -221,18 +252,18 @@ class CompositeKey private constructor (val threshold: Int, * Builds the [CompositeKey]. If [threshold] is not specified, it will default to * the total (aggregated) weight of the children, effectively generating an "N of N" requirement. * During process removes single keys wrapped in [CompositeKey] and enforces ordering on child nodes. + * + * @throws IllegalArgumentException */ - @Throws(IllegalArgumentException::class) fun build(threshold: Int? = null): PublicKey { val n = children.size - if (n > 1) - return CompositeKey(threshold ?: children.map { (_, weight) -> weight }.sum(), children) + return if (n > 1) + CompositeKey(threshold ?: children.map { (_, weight) -> weight }.sum(), children) else if (n == 1) { require(threshold == null || threshold == children.first().weight) - { "Trying to build invalid CompositeKey, threshold value different than weight of single child node." } - return children.first().node // We can assume that this node is a correct CompositeKey. - } - else throw IllegalArgumentException("Trying to build CompositeKey without child nodes.") + { "Trying to build invalid CompositeKey, threshold value different than weight of single child node." } + children.first().node // We can assume that this node is a correct CompositeKey. + } else throw IllegalArgumentException("Trying to build CompositeKey without child nodes.") } } } diff --git a/core/src/main/kotlin/net/corda/core/crypto/CompositeSignature.kt b/core/src/main/kotlin/net/corda/core/crypto/composite/CompositeSignature.kt similarity index 87% rename from core/src/main/kotlin/net/corda/core/crypto/CompositeSignature.kt rename to core/src/main/kotlin/net/corda/core/crypto/composite/CompositeSignature.kt index 328af22603..099875b39c 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CompositeSignature.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/composite/CompositeSignature.kt @@ -1,4 +1,4 @@ -package net.corda.core.crypto +package net.corda.core.crypto.composite import net.corda.core.serialization.deserialize import org.bouncycastle.asn1.ASN1ObjectIdentifier @@ -10,14 +10,10 @@ import java.security.spec.AlgorithmParameterSpec /** * Dedicated class for storing a set of signatures that comprise [CompositeKey]. */ -class CompositeSignature : Signature(ALGORITHM) { +class CompositeSignature : Signature(SIGNATURE_ALGORITHM) { companion object { - val ALGORITHM = "2.25.30086077608615255153862931087626791003" - // UUID-based OID - // TODO: Register for an OID space and issue our own shorter OID - val ALGORITHM_IDENTIFIER = AlgorithmIdentifier(ASN1ObjectIdentifier(ALGORITHM)) - - fun getService(provider: Provider) = Provider.Service(provider, "Signature", ALGORITHM, CompositeSignature::class.java.name, emptyList(), emptyMap()) + val SIGNATURE_ALGORITHM = "COMPOSITESIG" + fun getService(provider: Provider) = Provider.Service(provider, "Signature", SIGNATURE_ALGORITHM, CompositeSignature::class.java.name, emptyList(), emptyMap()) } private var signatureState: State? = null diff --git a/core/src/main/kotlin/net/corda/core/crypto/CompositeSignaturesWithKeys.kt b/core/src/main/kotlin/net/corda/core/crypto/composite/CompositeSignaturesWithKeys.kt similarity index 83% rename from core/src/main/kotlin/net/corda/core/crypto/CompositeSignaturesWithKeys.kt rename to core/src/main/kotlin/net/corda/core/crypto/composite/CompositeSignaturesWithKeys.kt index 6edac6ce43..5a69484ffa 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CompositeSignaturesWithKeys.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/composite/CompositeSignaturesWithKeys.kt @@ -1,5 +1,6 @@ -package net.corda.core.crypto +package net.corda.core.crypto.composite +import net.corda.core.crypto.DigitalSignature import net.corda.core.serialization.CordaSerializable /** diff --git a/core/src/main/kotlin/net/corda/core/crypto/composite/KeyFactory.kt b/core/src/main/kotlin/net/corda/core/crypto/composite/KeyFactory.kt new file mode 100644 index 0000000000..b933188d88 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/crypto/composite/KeyFactory.kt @@ -0,0 +1,34 @@ +package net.corda.core.crypto.composite + +import java.security.* +import java.security.spec.InvalidKeySpecException +import java.security.spec.KeySpec +import java.security.spec.X509EncodedKeySpec + +class KeyFactory : KeyFactorySpi() { + + @Throws(InvalidKeySpecException::class) + override fun engineGeneratePrivate(keySpec: KeySpec): PrivateKey { + // Private composite key not supported. + throw InvalidKeySpecException("key spec not recognised: " + keySpec.javaClass) + } + + @Throws(InvalidKeySpecException::class) + override fun engineGeneratePublic(keySpec: KeySpec): PublicKey? { + return when (keySpec) { + is X509EncodedKeySpec -> CompositeKey.getInstance(keySpec.encoded) + else -> throw InvalidKeySpecException("key spec not recognised: " + keySpec.javaClass) + } + } + + @Throws(InvalidKeySpecException::class) + override fun engineGetKeySpec(key: Key, keySpec: Class): T { + // Only support [X509EncodedKeySpec]. + throw InvalidKeySpecException("Not implemented yet $key $keySpec") + } + + @Throws(InvalidKeyException::class) + override fun engineTranslateKey(key: Key): Key { + throw InvalidKeyException("No other composite key providers known") + } +} diff --git a/core/src/main/kotlin/net/corda/core/crypto/provider/CordaSecurityProvider.kt b/core/src/main/kotlin/net/corda/core/crypto/provider/CordaSecurityProvider.kt new file mode 100644 index 0000000000..7951142336 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/crypto/provider/CordaSecurityProvider.kt @@ -0,0 +1,37 @@ +package net.corda.core.crypto.provider + +import net.corda.core.crypto.composite.CompositeKey +import net.corda.core.crypto.composite.CompositeSignature +import org.bouncycastle.asn1.ASN1ObjectIdentifier +import org.bouncycastle.asn1.x509.AlgorithmIdentifier +import java.security.AccessController +import java.security.PrivilegedAction +import java.security.Provider + +class CordaSecurityProvider : Provider(PROVIDER_NAME, 0.1, "$PROVIDER_NAME security provider wrapper") { + companion object { + val PROVIDER_NAME = "Corda" + } + + init { + AccessController.doPrivileged(PrivilegedAction { setup() }) + } + + private fun setup() { + put("KeyFactory.${CompositeKey.KEY_ALGORITHM}", "net.corda.core.crypto.composite.KeyFactory") + put("Signature.${CompositeSignature.SIGNATURE_ALGORITHM}", "net.corda.core.crypto.composite.CompositeSignature") + + val compositeKeyOID = CordaObjectIdentifier.compositeKey.id + put("Alg.Alias.KeyFactory.$compositeKeyOID", CompositeKey.KEY_ALGORITHM) + put("Alg.Alias.KeyFactory.OID.$compositeKeyOID", CompositeKey.KEY_ALGORITHM) + put("Alg.Alias.Signature.$compositeKeyOID", CompositeSignature.SIGNATURE_ALGORITHM) + put("Alg.Alias.Signature.OID.$compositeKeyOID", CompositeSignature.SIGNATURE_ALGORITHM) + } +} + +object CordaObjectIdentifier { + // UUID-based OID + // TODO: Register for an OID space and issue our own shorter OID + val compositeKey = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791002") + val compositeSignature = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791003") +} diff --git a/core/src/main/kotlin/net/corda/core/node/services/Services.kt b/core/src/main/kotlin/net/corda/core/node/services/Services.kt index e4547d495d..6d1a486105 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/Services.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/Services.kt @@ -3,7 +3,7 @@ package net.corda.core.node.services import co.paralleluniverse.fibers.Suspendable import com.google.common.util.concurrent.ListenableFuture import net.corda.core.contracts.* -import net.corda.core.crypto.CompositeKey +import net.corda.core.crypto.composite.CompositeKey import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.SecureHash import net.corda.core.crypto.keys diff --git a/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt b/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt index f32d1e9d2b..9b1a18be0c 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt @@ -8,7 +8,7 @@ import de.javakaffee.kryoserializers.ArraysAsListSerializer import de.javakaffee.kryoserializers.BitSetSerializer import de.javakaffee.kryoserializers.UnmodifiableCollectionsSerializer import de.javakaffee.kryoserializers.guava.* -import net.corda.core.crypto.CompositeKey +import net.corda.core.crypto.composite.CompositeKey import net.corda.core.crypto.MetaData import net.corda.core.node.CordaPluginRegistry import net.corda.core.transactions.SignedTransaction diff --git a/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt b/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt index 5b043cd33b..27b03c66ac 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt @@ -9,6 +9,7 @@ import com.esotericsoftware.kryo.util.MapReferenceResolver import com.google.common.annotations.VisibleForTesting import net.corda.core.contracts.* import net.corda.core.crypto.* +import net.corda.core.crypto.composite.CompositeKey import net.corda.core.identity.Party import net.corda.core.node.AttachmentsClassLoader import net.corda.core.transactions.WireTransaction diff --git a/core/src/test/kotlin/net/corda/core/contracts/TransactionTests.kt b/core/src/test/kotlin/net/corda/core/contracts/TransactionTests.kt index ffb3faeb4c..d4308446e2 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/TransactionTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/TransactionTests.kt @@ -2,7 +2,7 @@ package net.corda.core.contracts import net.corda.contracts.asset.DUMMY_CASH_ISSUER_KEY import net.corda.testing.contracts.DummyContract -import net.corda.core.crypto.CompositeKey +import net.corda.core.crypto.composite.CompositeKey import net.corda.core.crypto.SecureHash import net.corda.core.crypto.generateKeyPair import net.corda.core.crypto.sign diff --git a/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt b/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt index bee35e0a9d..5294898acd 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt @@ -1,14 +1,25 @@ package net.corda.core.crypto +import net.corda.core.crypto.composite.CompositeKey +import net.corda.core.crypto.composite.CompositeSignature +import net.corda.core.crypto.composite.CompositeSignaturesWithKeys +import net.corda.core.div import net.corda.core.serialization.serialize import net.corda.core.utilities.OpaqueBytes +import org.bouncycastle.asn1.x500.X500Name +import org.junit.Rule import org.junit.Test +import org.junit.rules.TemporaryFolder import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertFalse import kotlin.test.assertTrue class CompositeKeyTests { + @Rule + @JvmField + val tempFolder: TemporaryFolder = TemporaryFolder() + val aliceKey = generateKeyPair() val bobKey = generateKeyPair() val charlieKey = generateKeyPair() @@ -65,7 +76,7 @@ class CompositeKeyTests { } @Test - fun `encoded tree decodes correctly`() { + fun `kryo encoded tree decodes correctly`() { val aliceAndBob = CompositeKey.Builder().addKeys(alicePublicKey, bobPublicKey).build() val aliceAndBobOrCharlie = CompositeKey.Builder().addKeys(aliceAndBob, charliePublicKey).build(threshold = 1) @@ -75,6 +86,35 @@ class CompositeKeyTests { assertEquals(decoded, aliceAndBobOrCharlie) } + @Test + fun `der encoded tree decodes correctly`() { + val aliceAndBob = CompositeKey.Builder().addKeys(alicePublicKey, bobPublicKey).build() + val aliceAndBobOrCharlie = CompositeKey.Builder().addKeys(aliceAndBob, charliePublicKey).build(threshold = 1) + + val encoded = aliceAndBobOrCharlie.encoded + val decoded = CompositeKey.getInstance(encoded) + + assertEquals(decoded, aliceAndBobOrCharlie) + } + + @Test + fun `der encoded tree decodes correctly with weighting`() { + val aliceAndBob = CompositeKey.Builder() + .addKey(alicePublicKey, 2) + .addKey(bobPublicKey, 1) + .build(threshold = 2) + + val aliceAndBobOrCharlie = CompositeKey.Builder() + .addKey(aliceAndBob, 3) + .addKey(charliePublicKey, 2) + .build(threshold = 3) + + val encoded = aliceAndBobOrCharlie.encoded + val decoded = CompositeKey.getInstance(encoded) + + assertEquals(decoded, aliceAndBobOrCharlie) + } + @Test fun `tree canonical form`() { assertEquals(CompositeKey.Builder().addKeys(alicePublicKey).build(), alicePublicKey) @@ -260,6 +300,59 @@ class CompositeKeyTests { assertFalse { compositeKey.isFulfilledBy(signaturesWithoutRSA.byKeys()) } } + @Test + fun `Test save to keystore`() { + // From test case [CompositeKey from multiple signature schemes and signature verification] + val (privRSA, pubRSA) = Crypto.generateKeyPair(Crypto.RSA_SHA256) + val (privK1, pubK1) = Crypto.generateKeyPair(Crypto.ECDSA_SECP256K1_SHA256) + val (privR1, pubR1) = Crypto.generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256) + val (privEd, pubEd) = Crypto.generateKeyPair(Crypto.EDDSA_ED25519_SHA512) + val (privSP, pubSP) = Crypto.generateKeyPair(Crypto.SPHINCS256_SHA256) + + val RSASignature = privRSA.sign(message.bytes, pubRSA) + val K1Signature = privK1.sign(message.bytes, pubK1) + val R1Signature = privR1.sign(message.bytes, pubR1) + val EdSignature = privEd.sign(message.bytes, pubEd) + val SPSignature = privSP.sign(message.bytes, pubSP) + + val compositeKey = CompositeKey.Builder().addKeys(pubRSA, pubK1, pubR1, pubEd, pubSP).build() as CompositeKey + + val signatures = listOf(RSASignature, K1Signature, R1Signature, EdSignature, SPSignature) + assertTrue { compositeKey.isFulfilledBy(signatures.byKeys()) } + // One signature is missing. + val signaturesWithoutRSA = listOf(K1Signature, R1Signature, EdSignature, SPSignature) + assertFalse { compositeKey.isFulfilledBy(signaturesWithoutRSA.byKeys()) } + + // Create self sign CA. + val caKeyPair = Crypto.generateKeyPair() + val ca = X509Utilities.createSelfSignedCACertificate(X500Name("CN=Test CA"), caKeyPair) + + // Sign the composite key with the self sign CA. + val compositeKeyCert = X509Utilities.createCertificate(CertificateType.IDENTITY, ca, caKeyPair, X500Name("CN=CompositeKey"), compositeKey) + + // Store certificate to keystore. + val keystorePath = tempFolder.root.toPath() / "keystore.jks" + val keystore = KeyStoreUtilities.loadOrCreateKeyStore(keystorePath, "password") + keystore.setCertificateEntry("CompositeKey", compositeKeyCert.cert) + keystore.save(keystorePath, "password") + + // Load keystore from disk. + val keystore2 = KeyStoreUtilities.loadKeyStore(keystorePath, "password") + assertTrue { keystore2.containsAlias("CompositeKey") } + + val key = keystore2.getCertificate("CompositeKey").publicKey + // Convert sun public key to Composite key. + val compositeKey2 = Crypto.toSupportedPublicKey(key) + assertTrue { compositeKey2 is CompositeKey } + + // Run the same composite key test again. + assertTrue { compositeKey2.isFulfilledBy(signatures.byKeys()) } + assertFalse { compositeKey2.isFulfilledBy(signaturesWithoutRSA.byKeys()) } + + // Ensure keys are the same before and after keystore. + assertEquals(compositeKey, compositeKey2) + } + @Test fun `CompositeKey deterministic children sorting`() { val (_, pub1) = Crypto.generateKeyPair(Crypto.EDDSA_ED25519_SHA512) diff --git a/core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt b/core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt index 7a45fe7ac3..5468886603 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt @@ -344,7 +344,7 @@ class CryptoUtilsTest { @Test fun `Check supported algorithms`() { val algList: List = Crypto.supportedSignatureSchemes.keys.toList() - val expectedAlgSet = setOf("RSA_SHA256", "ECDSA_SECP256K1_SHA256", "ECDSA_SECP256R1_SHA256", "EDDSA_ED25519_SHA512", "SPHINCS-256_SHA512") + val expectedAlgSet = setOf("RSA_SHA256", "ECDSA_SECP256K1_SHA256", "ECDSA_SECP256R1_SHA256", "EDDSA_ED25519_SHA512", "SPHINCS-256_SHA512", "COMPOSITE") assertTrue { Sets.symmetricDifference(expectedAlgSet, algList.toSet()).isEmpty(); } } diff --git a/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt b/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt index 879f7dd5f0..58a7b3b95d 100644 --- a/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt +++ b/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt @@ -9,7 +9,7 @@ import io.requery.sql.* import io.requery.sql.platform.Generic import net.corda.core.contracts.* import net.corda.testing.contracts.DummyContract -import net.corda.core.crypto.CompositeKey +import net.corda.core.crypto.composite.CompositeKey import net.corda.core.crypto.SecureHash import net.corda.core.crypto.generateKeyPair import net.corda.core.crypto.toBase58String diff --git a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt index c01b7e7fd9..7e7060cbc6 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt @@ -4,7 +4,8 @@ import com.nhaarman.mockito_kotlin.whenever import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateRef import net.corda.core.contracts.TransactionType -import net.corda.core.crypto.CompositeKey +import net.corda.testing.contracts.DummyContract +import net.corda.core.crypto.composite.CompositeKey import net.corda.core.crypto.SecureHash import net.corda.core.div import net.corda.core.getOrThrow @@ -22,7 +23,6 @@ import net.corda.node.services.transactions.minClusterSize import net.corda.node.services.transactions.minCorrectReplicas import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.node.utilities.transaction -import net.corda.testing.contracts.DummyContract import net.corda.testing.node.MockNetwork import org.bouncycastle.asn1.x500.X500Name import org.junit.After diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 2f8d586e10..b05b0d6c33 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -10,6 +10,7 @@ import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult import net.corda.core.* import net.corda.core.crypto.* +import net.corda.core.crypto.composite.CompositeKey import net.corda.core.flows.* import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate diff --git a/node/src/main/kotlin/net/corda/node/utilities/ServiceIdentityGenerator.kt b/node/src/main/kotlin/net/corda/node/utilities/ServiceIdentityGenerator.kt index 6629f7ca26..2f2db9d09b 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/ServiceIdentityGenerator.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/ServiceIdentityGenerator.kt @@ -1,7 +1,6 @@ package net.corda.node.utilities -import net.corda.core.crypto.CompositeKey -import net.corda.core.crypto.X509Utilities +import net.corda.core.crypto.composite.CompositeKey import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.Party import net.corda.core.serialization.serialize diff --git a/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt b/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt index 3755f92b22..ea574f79a0 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt @@ -2,6 +2,7 @@ package net.corda.testing import net.corda.core.contracts.* import net.corda.core.crypto.* +import net.corda.core.crypto.composite.expandedCompositeKeys import net.corda.core.crypto.testing.NullSignature import net.corda.core.identity.Party import net.corda.core.node.ServiceHub From d6deeb2bd6c6a2ddbc89550f94ab5f9c1cce7454 Mon Sep 17 00:00:00 2001 From: josecoll Date: Wed, 12 Jul 2017 18:11:04 +0100 Subject: [PATCH 91/97] Fixed failing SmokeTests using Vault Query (#1032) * Fixed failing SmokeTest caused by incorrect default count filter. * Fixed incorrect spend value for expected assertion. * Remove deprecated test (unintentionally merged after rebase from master) --- .../kotlin/rpc/StandaloneCordaRPClientTest.kt | 25 +------ .../services/vault/HibernateVaultQueryImpl.kt | 2 +- .../node/services/vault/VaultQueryTests.kt | 67 +++++++++++++++++++ 3 files changed, 69 insertions(+), 25 deletions(-) diff --git a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt index a01cd27c5b..182ca10eea 100644 --- a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt +++ b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt @@ -134,33 +134,10 @@ class StandaloneCordaRPClientTest { assertEquals(1, updateCount) } - @Test - fun `test vault`() { - val (vault, vaultUpdates) = rpcProxy.vaultAndUpdates() - assertEquals(0, vault.size) - - var updateCount = 0 - vaultUpdates.subscribe { update -> - log.info("Vault>> FlowId=${update.flowId}") - ++updateCount - } - - // Now issue some cash - rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryNode.legalIdentity, notaryNode.notaryIdentity) - .returnValue.getOrThrow(timeout) - assertNotEquals(0, updateCount) - - // Check that this cash exists in the vault - val cashBalance = rpcProxy.getCashBalances() - log.info("Cash Balances: $cashBalance") - assertEquals(1, cashBalance.size) - assertEquals(629.POUNDS, cashBalance[Currency.getInstance("GBP")]) - } - @Test fun `test vault track by`() { val (vault, vaultUpdates) = rpcProxy.vaultTrackBy() - assertEquals(0, vault.totalStatesAvailable) + assertEquals(0, vault.states.size) var updateCount = 0 vaultUpdates.subscribe { update -> diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt index 2d10eaaf63..c325c673b4 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt @@ -45,7 +45,7 @@ class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration, var totalStates = -1L if (!paging.isDefault) { val count = builder { VaultSchemaV1.VaultStates::recordedTime.count() } - val countCriteria = VaultCustomQueryCriteria(count) + val countCriteria = VaultCustomQueryCriteria(count, Vault.StateStatus.ALL) val results = queryBy(contractType, criteria.and(countCriteria)) totalStates = results.otherResults[0] as Long } diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index 997da4b345..42b43e1ce3 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -187,6 +187,30 @@ class VaultQueryTests { } } + @Test + fun `unconsumed states with count`() { + database.transaction { + + services.fillWithSomeTestCash(25.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(25.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(25.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(25.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + + val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) + val paging = PageSpecification(DEFAULT_PAGE_NUM, 10) + val resultsBeforeConsume = vaultQuerySvc.queryBy(criteria, paging) + assertThat(resultsBeforeConsume.states).hasSize(4) + assertThat(resultsBeforeConsume.totalStatesAvailable).isEqualTo(4) + + services.consumeCash(75.DOLLARS) + + val consumedCriteria = VaultQueryCriteria(status = Vault.StateStatus.UNCONSUMED) + val resultsAfterConsume = vaultQuerySvc.queryBy(consumedCriteria, paging) + assertThat(resultsAfterConsume.states).hasSize(1) + assertThat(resultsAfterConsume.totalStatesAvailable).isEqualTo(1) + } + } + @Test fun `unconsumed cash states simple`() { database.transaction { @@ -331,6 +355,30 @@ class VaultQueryTests { } } + @Test + fun `consumed states with count`() { + database.transaction { + + services.fillWithSomeTestCash(25.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(25.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(25.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(25.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + + val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) + val paging = PageSpecification(DEFAULT_PAGE_NUM, 10) + val resultsBeforeConsume = vaultQuerySvc.queryBy(criteria, paging) + assertThat(resultsBeforeConsume.states).hasSize(4) + assertThat(resultsBeforeConsume.totalStatesAvailable).isEqualTo(4) + + services.consumeCash(75.DOLLARS) + + val consumedCriteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) + val resultsAfterConsume = vaultQuerySvc.queryBy(consumedCriteria, paging) + assertThat(resultsAfterConsume.states).hasSize(3) + assertThat(resultsAfterConsume.totalStatesAvailable).isEqualTo(3) + } + } + @Test fun `all states`() { database.transaction { @@ -349,6 +397,25 @@ class VaultQueryTests { } } + @Test + fun `all states with count`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + + val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) + val paging = PageSpecification(DEFAULT_PAGE_NUM, 10) + val resultsBeforeConsume = vaultQuerySvc.queryBy(criteria, paging) + assertThat(resultsBeforeConsume.states).hasSize(1) + assertThat(resultsBeforeConsume.totalStatesAvailable).isEqualTo(1) + + services.consumeCash(50.DOLLARS) // consumed 100 (spent), produced 50 (change) + + val resultsAfterConsume = vaultQuerySvc.queryBy(criteria, paging) + assertThat(resultsAfterConsume.states).hasSize(2) + assertThat(resultsAfterConsume.totalStatesAvailable).isEqualTo(2) + } + } val CASH_NOTARY_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(20)) } val CASH_NOTARY: Party get() = Party(X500Name("CN=Cash Notary Service,O=R3,OU=corda,L=Zurich,C=CH"), CASH_NOTARY_KEY.public) From 6e570b4d65b8655d140a127c42366845cce402cc Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Wed, 12 Jul 2017 14:09:49 +0100 Subject: [PATCH 92/97] Moved dummy stuff from core into test-utils --- .../schemas/testing/DummyDealStateSchemaV1.kt | 25 -------------- .../testing/DummyLinearStateSchemaV2.kt | 24 -------------- .../services/vault/VaultQueryJavaTests.java | 2 +- .../database/HibernateConfigurationTest.kt | 4 +-- .../node/services/vault/VaultQueryTests.kt | 8 ++--- .../node/services/vault/VaultWithCashTest.kt | 2 +- test-utils/build.gradle | 1 + .../testing}/contracts/DummyDealContract.kt | 5 +-- .../testing/contracts/DummyLinearContract.kt | 4 +-- .../corda/testing/contracts/VaultFiller.kt | 1 - .../testing/schemas/DummyDealStateSchemaV1.kt | 33 +++++++++++++++++++ .../schemas}/DummyLinearStateSchemaV1.kt | 2 +- .../schemas/DummyLinearStateSchemaV2.kt | 31 +++++++++++++++++ 13 files changed, 78 insertions(+), 64 deletions(-) delete mode 100644 core/src/main/kotlin/net/corda/core/schemas/testing/DummyDealStateSchemaV1.kt delete mode 100644 core/src/main/kotlin/net/corda/core/schemas/testing/DummyLinearStateSchemaV2.kt rename {finance/src/main/kotlin/net/corda => test-utils/src/main/kotlin/net/corda/testing}/contracts/DummyDealContract.kt (93%) create mode 100644 test-utils/src/main/kotlin/net/corda/testing/schemas/DummyDealStateSchemaV1.kt rename {core/src/main/kotlin/net/corda/core/schemas/testing => test-utils/src/main/kotlin/net/corda/testing/schemas}/DummyLinearStateSchemaV1.kt (97%) create mode 100644 test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV2.kt diff --git a/core/src/main/kotlin/net/corda/core/schemas/testing/DummyDealStateSchemaV1.kt b/core/src/main/kotlin/net/corda/core/schemas/testing/DummyDealStateSchemaV1.kt deleted file mode 100644 index 4b7a2a5b68..0000000000 --- a/core/src/main/kotlin/net/corda/core/schemas/testing/DummyDealStateSchemaV1.kt +++ /dev/null @@ -1,25 +0,0 @@ -package net.corda.core.schemas.testing - -/** - * An object used to fully qualify the [DummyDealStateSchema] family name (i.e. independent of version). - */ -object DummyDealStateSchema - -/** - * First version of a cash contract ORM schema that maps all fields of the [DummyDealState] contract state as it stood - * at the time of writing. - */ -object DummyDealStateSchemaV1 : net.corda.core.schemas.MappedSchema(schemaFamily = net.corda.core.schemas.testing.DummyDealStateSchema.javaClass, version = 1, mappedTypes = listOf(net.corda.core.schemas.testing.DummyDealStateSchemaV1.PersistentDummyDealState::class.java)) { - @javax.persistence.Entity - @javax.persistence.Table(name = "dummy_deal_states") - class PersistentDummyDealState( - - @javax.persistence.Column(name = "deal_reference") - var dealReference: String, - - /** parent attributes */ - @javax.persistence.Transient - val uid: net.corda.core.contracts.UniqueIdentifier - - ) : net.corda.node.services.vault.schemas.jpa.CommonSchemaV1.LinearState(uid = uid) -} diff --git a/core/src/main/kotlin/net/corda/core/schemas/testing/DummyLinearStateSchemaV2.kt b/core/src/main/kotlin/net/corda/core/schemas/testing/DummyLinearStateSchemaV2.kt deleted file mode 100644 index ec2f36d86c..0000000000 --- a/core/src/main/kotlin/net/corda/core/schemas/testing/DummyLinearStateSchemaV2.kt +++ /dev/null @@ -1,24 +0,0 @@ -package net.corda.core.schemas.testing - -/** - * Second version of a cash contract ORM schema that extends the common - * [VaultLinearState] abstract schema - */ -object DummyLinearStateSchemaV2 : net.corda.core.schemas.MappedSchema(schemaFamily = DummyLinearStateSchema.javaClass, version = 2, - mappedTypes = listOf(net.corda.core.schemas.testing.DummyLinearStateSchemaV2.PersistentDummyLinearState::class.java)) { - @javax.persistence.Entity - @javax.persistence.Table(name = "dummy_linear_states_v2") - class PersistentDummyLinearState( - @javax.persistence.Column(name = "linear_string") var linearString: String, - - @javax.persistence.Column(name = "linear_number") var linearNumber: Long, - - @javax.persistence.Column(name = "linear_timestamp") var linearTimestamp: java.time.Instant, - - @javax.persistence.Column(name = "linear_boolean") var linearBoolean: Boolean, - - /** parent attributes */ - @Transient - val uid: net.corda.core.contracts.UniqueIdentifier - ) : net.corda.node.services.vault.schemas.jpa.CommonSchemaV1.LinearState(uid = uid) -} diff --git a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java index 512a0ab5d3..f5b44a3487 100644 --- a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java +++ b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java @@ -12,7 +12,6 @@ import net.corda.core.node.services.*; import net.corda.core.node.services.vault.*; import net.corda.core.node.services.vault.QueryCriteria.*; import net.corda.core.schemas.*; -import net.corda.core.schemas.testing.*; import net.corda.core.transactions.*; import net.corda.core.utilities.*; import net.corda.node.services.database.*; @@ -21,6 +20,7 @@ import net.corda.schemas.*; import net.corda.testing.*; import net.corda.testing.contracts.*; import net.corda.testing.node.*; +import net.corda.testing.schemas.DummyLinearStateSchemaV1; import org.jetbrains.annotations.*; import org.jetbrains.exposed.sql.*; import org.junit.*; diff --git a/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt index 9081d6e72f..48e56ddc55 100644 --- a/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt @@ -12,8 +12,8 @@ import net.corda.core.crypto.toBase58String import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultService import net.corda.core.schemas.PersistentStateRef -import net.corda.core.schemas.testing.DummyLinearStateSchemaV1 -import net.corda.core.schemas.testing.DummyLinearStateSchemaV2 +import net.corda.testing.schemas.DummyLinearStateSchemaV1 +import net.corda.testing.schemas.DummyLinearStateSchemaV2 import net.corda.core.serialization.deserialize import net.corda.core.serialization.storageKryo import net.corda.core.transactions.SignedTransaction diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index 42b43e1ce3..b669b07f6d 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -6,7 +6,6 @@ import net.corda.contracts.DealState import net.corda.contracts.asset.Cash import net.corda.contracts.asset.DUMMY_CASH_ISSUER import net.corda.core.contracts.* -import net.corda.testing.contracts.DummyLinearContract import net.corda.core.crypto.entropyToKeyPair import net.corda.core.crypto.toBase58String import net.corda.core.days @@ -14,10 +13,9 @@ import net.corda.core.identity.Party import net.corda.core.node.services.* import net.corda.core.node.services.vault.* import net.corda.core.node.services.vault.QueryCriteria.* -import net.corda.core.schemas.testing.DummyLinearStateSchemaV1 import net.corda.core.seconds -import net.corda.core.utilities.OpaqueBytes import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.toHexString import net.corda.node.services.database.HibernateConfiguration import net.corda.node.services.schema.NodeSchemaService @@ -32,6 +30,7 @@ import net.corda.testing.* import net.corda.testing.contracts.* import net.corda.testing.node.MockServices import net.corda.testing.node.makeTestDataSourceProperties +import net.corda.testing.schemas.DummyLinearStateSchemaV1 import org.assertj.core.api.Assertions import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy @@ -48,7 +47,6 @@ import java.time.LocalDate import java.time.ZoneOffset import java.time.temporal.ChronoUnit import java.util.* -import kotlin.test.assertFails class VaultQueryTests { @@ -974,7 +972,7 @@ class VaultQueryTests { assertThat(states).hasSize(20) assertThat(metadata.first().contractStateClassName).isEqualTo("net.corda.testing.contracts.DummyLinearContract\$State") assertThat(metadata.first().status).isEqualTo(Vault.StateStatus.UNCONSUMED) // 0 = UNCONSUMED - assertThat(metadata.last().contractStateClassName).isEqualTo("net.corda.contracts.DummyDealContract\$State") + assertThat(metadata.last().contractStateClassName).isEqualTo("net.corda.contracts.asset.Cash\$State") assertThat(metadata.last().status).isEqualTo(Vault.StateStatus.CONSUMED) // 1 = CONSUMED } } diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt index 02686c72b7..4ca05bb8e9 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt @@ -1,6 +1,6 @@ package net.corda.node.services.vault -import net.corda.contracts.DummyDealContract +import net.corda.testing.contracts.DummyDealContract import net.corda.contracts.asset.Cash import net.corda.contracts.asset.DUMMY_CASH_ISSUER import net.corda.testing.contracts.fillWithSomeTestCash diff --git a/test-utils/build.gradle b/test-utils/build.gradle index c0567edec9..f6801db7ef 100644 --- a/test-utils/build.gradle +++ b/test-utils/build.gradle @@ -1,4 +1,5 @@ apply plugin: 'kotlin' +apply plugin: 'kotlin-jpa' apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'com.jfrog.artifactory' diff --git a/finance/src/main/kotlin/net/corda/contracts/DummyDealContract.kt b/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyDealContract.kt similarity index 93% rename from finance/src/main/kotlin/net/corda/contracts/DummyDealContract.kt rename to test-utils/src/main/kotlin/net/corda/testing/contracts/DummyDealContract.kt index 39041fbef5..57d155f38d 100644 --- a/finance/src/main/kotlin/net/corda/contracts/DummyDealContract.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyDealContract.kt @@ -1,5 +1,6 @@ -package net.corda.contracts +package net.corda.testing.contracts +import net.corda.contracts.DealState import net.corda.core.contracts.Contract import net.corda.core.contracts.TransactionForContract import net.corda.core.contracts.UniqueIdentifier @@ -10,7 +11,7 @@ import net.corda.core.identity.Party import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState import net.corda.core.schemas.QueryableState -import net.corda.core.schemas.testing.DummyDealStateSchemaV1 +import net.corda.testing.schemas.DummyDealStateSchemaV1 import net.corda.core.transactions.TransactionBuilder import java.security.PublicKey diff --git a/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyLinearContract.kt b/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyLinearContract.kt index 46519730cf..4f0d31d676 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyLinearContract.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyLinearContract.kt @@ -10,8 +10,8 @@ import net.corda.core.identity.AbstractParty import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState import net.corda.core.schemas.QueryableState -import net.corda.core.schemas.testing.DummyLinearStateSchemaV1 -import net.corda.core.schemas.testing.DummyLinearStateSchemaV2 +import net.corda.testing.schemas.DummyLinearStateSchemaV1 +import net.corda.testing.schemas.DummyLinearStateSchemaV2 import java.time.LocalDateTime import java.time.ZoneOffset.UTC diff --git a/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt b/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt index d7ae9ff523..0994505b0b 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt @@ -4,7 +4,6 @@ package net.corda.testing.contracts import net.corda.contracts.Commodity import net.corda.contracts.DealState -import net.corda.contracts.DummyDealContract import net.corda.contracts.asset.* import net.corda.core.contracts.* import net.corda.core.identity.AbstractParty diff --git a/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyDealStateSchemaV1.kt b/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyDealStateSchemaV1.kt new file mode 100644 index 0000000000..b28e8cfe44 --- /dev/null +++ b/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyDealStateSchemaV1.kt @@ -0,0 +1,33 @@ +package net.corda.testing.schemas + +import net.corda.core.contracts.UniqueIdentifier +import net.corda.core.schemas.MappedSchema +import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1 +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.Table +import javax.persistence.Transient + +/** + * An object used to fully qualify the [DummyDealStateSchema] family name (i.e. independent of version). + */ +object DummyDealStateSchema + +/** + * First version of a cash contract ORM schema that maps all fields of the [DummyDealState] contract state as it stood + * at the time of writing. + */ +object DummyDealStateSchemaV1 : MappedSchema(schemaFamily = DummyDealStateSchema.javaClass, version = 1, mappedTypes = listOf(PersistentDummyDealState::class.java)) { + @Entity + @Table(name = "dummy_deal_states") + class PersistentDummyDealState( + + @Column(name = "deal_reference") + var dealReference: String, + + /** parent attributes */ + @Transient + val uid: UniqueIdentifier + + ) : CommonSchemaV1.LinearState(uid = uid) +} diff --git a/core/src/main/kotlin/net/corda/core/schemas/testing/DummyLinearStateSchemaV1.kt b/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV1.kt similarity index 97% rename from core/src/main/kotlin/net/corda/core/schemas/testing/DummyLinearStateSchemaV1.kt rename to test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV1.kt index 3a0b8e3a66..b8e490b04f 100644 --- a/core/src/main/kotlin/net/corda/core/schemas/testing/DummyLinearStateSchemaV1.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV1.kt @@ -1,4 +1,4 @@ -package net.corda.core.schemas.testing +package net.corda.testing.schemas import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState diff --git a/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV2.kt b/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV2.kt new file mode 100644 index 0000000000..91f3e49cbf --- /dev/null +++ b/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV2.kt @@ -0,0 +1,31 @@ +package net.corda.testing.schemas + +import net.corda.core.contracts.UniqueIdentifier +import net.corda.core.schemas.MappedSchema +import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1 +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.Table + +/** + * Second version of a cash contract ORM schema that extends the common + * [VaultLinearState] abstract schema + */ +object DummyLinearStateSchemaV2 : MappedSchema(schemaFamily = DummyLinearStateSchema.javaClass, version = 2, + mappedTypes = listOf(PersistentDummyLinearState::class.java)) { + @Entity + @Table(name = "dummy_linear_states_v2") + class PersistentDummyLinearState( + @Column(name = "linear_string") var linearString: String, + + @Column(name = "linear_number") var linearNumber: Long, + + @Column(name = "linear_timestamp") var linearTimestamp: java.time.Instant, + + @Column(name = "linear_boolean") var linearBoolean: Boolean, + + /** parent attributes */ + @Transient + val uid: UniqueIdentifier + ) : CommonSchemaV1.LinearState(uid = uid) +} From 7eed258bcbe4e52cacd877820e024ab6fab44c1e Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Wed, 12 Jul 2017 15:46:28 +0100 Subject: [PATCH 93/97] Fixed incorrect package declarations in CommonSchema and VaultSchema --- .../kotlin/net/corda/core/schemas/CommonSchema.kt | 10 +++++----- .../src/main/kotlin/net/corda/schemas/CashSchemaV1.kt | 1 - .../kotlin/net/corda/schemas/SampleCashSchemaV1.kt | 1 - .../kotlin/net/corda/schemas/SampleCashSchemaV2.kt | 2 +- .../kotlin/net/corda/schemas/SampleCashSchemaV3.kt | 2 +- .../corda/schemas/SampleCommercialPaperSchemaV2.kt | 4 +--- .../corda/node/services/schema/NodeSchemaService.kt | 4 ++-- .../services/vault/HibernateQueryCriteriaParser.kt | 3 +-- .../node/services/vault/HibernateVaultQueryImpl.kt | 1 - .../net/corda/node/services/vault/NodeVaultService.kt | 11 +++++++++-- .../net/corda/node/services/vault/VaultSchema.kt | 3 ++- .../services/database/HibernateConfigurationTest.kt | 6 +++--- .../net/corda/node/services/vault/VaultQueryTests.kt | 1 - .../corda/testing/schemas/DummyDealStateSchemaV1.kt | 2 +- .../corda/testing/schemas/DummyLinearStateSchemaV2.kt | 2 +- 15 files changed, 27 insertions(+), 26 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/schemas/CommonSchema.kt b/core/src/main/kotlin/net/corda/core/schemas/CommonSchema.kt index 1fe600300a..a74001d912 100644 --- a/core/src/main/kotlin/net/corda/core/schemas/CommonSchema.kt +++ b/core/src/main/kotlin/net/corda/core/schemas/CommonSchema.kt @@ -1,11 +1,11 @@ -package net.corda.node.services.vault.schemas.jpa +package net.corda.core.schemas +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.FungibleAsset +import net.corda.core.contracts.OwnableState import net.corda.core.contracts.UniqueIdentifier import net.corda.core.crypto.toBase58String import net.corda.core.identity.AbstractParty -import net.corda.core.schemas.MappedSchema -import net.corda.core.schemas.PersistentState -import net.corda.core.schemas.StatePersistable import java.util.* import javax.persistence.* @@ -90,7 +90,7 @@ object CommonSchemaV1 : MappedSchema(schemaFamily = CommonSchema.javaClass, vers @Column(name = "party_key", length = 65535) // TODO What is the upper limit on size of CompositeKey?) var key: String ) { - constructor(party: net.corda.core.identity.AbstractParty) + constructor(party: AbstractParty) : this(0, party.nameOrNull()?.toString() ?: party.toString(), party.owningKey.toBase58String()) } } \ No newline at end of file diff --git a/finance/src/main/kotlin/net/corda/schemas/CashSchemaV1.kt b/finance/src/main/kotlin/net/corda/schemas/CashSchemaV1.kt index 05bc37c1c3..e2ede7a16e 100644 --- a/finance/src/main/kotlin/net/corda/schemas/CashSchemaV1.kt +++ b/finance/src/main/kotlin/net/corda/schemas/CashSchemaV1.kt @@ -2,7 +2,6 @@ package net.corda.schemas import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState -import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1 import javax.persistence.* /** diff --git a/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV1.kt b/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV1.kt index 08cda83d92..453dff0203 100644 --- a/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV1.kt +++ b/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV1.kt @@ -2,7 +2,6 @@ package net.corda.schemas import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState -import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1 import javax.persistence.* /** diff --git a/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV2.kt b/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV2.kt index a48b664023..83755a4110 100644 --- a/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV2.kt +++ b/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV2.kt @@ -2,7 +2,7 @@ package net.corda.schemas import net.corda.core.identity.AbstractParty import net.corda.core.schemas.MappedSchema -import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1 +import net.corda.core.schemas.CommonSchemaV1 import javax.persistence.Column import javax.persistence.Entity import javax.persistence.Index diff --git a/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV3.kt b/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV3.kt index 33bd6e2a19..d1d7e46d79 100644 --- a/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV3.kt +++ b/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV3.kt @@ -3,7 +3,7 @@ package net.corda.schemas import net.corda.core.identity.AbstractParty import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState -import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1 +import net.corda.core.schemas.CommonSchemaV1 import javax.persistence.* /** diff --git a/finance/src/test/kotlin/net/corda/schemas/SampleCommercialPaperSchemaV2.kt b/finance/src/test/kotlin/net/corda/schemas/SampleCommercialPaperSchemaV2.kt index e52df55695..735eda7afc 100644 --- a/finance/src/test/kotlin/net/corda/schemas/SampleCommercialPaperSchemaV2.kt +++ b/finance/src/test/kotlin/net/corda/schemas/SampleCommercialPaperSchemaV2.kt @@ -1,10 +1,8 @@ package net.corda.schemas -import net.corda.core.crypto.toBase58String import net.corda.core.identity.AbstractParty import net.corda.core.schemas.MappedSchema -import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1 -import java.security.PublicKey +import net.corda.core.schemas.CommonSchemaV1 import java.time.Instant import javax.persistence.Column import javax.persistence.Entity diff --git a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt index 03c9f35a7d..1483ac4e9a 100644 --- a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt +++ b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt @@ -9,8 +9,8 @@ import net.corda.core.schemas.PersistentState import net.corda.core.schemas.QueryableState import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.node.services.api.SchemaService -import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1 -import net.corda.node.services.vault.schemas.jpa.VaultSchemaV1 +import net.corda.core.schemas.CommonSchemaV1 +import net.corda.node.services.vault.VaultSchemaV1 import net.corda.schemas.CashSchemaV1 /** diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt index 9cc3fad23d..6ad1d928f5 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt @@ -14,8 +14,7 @@ import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.toHexString import net.corda.core.utilities.loggerFor import net.corda.core.utilities.trace -import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1 -import net.corda.node.services.vault.schemas.jpa.VaultSchemaV1 +import net.corda.core.schemas.CommonSchemaV1 import org.bouncycastle.asn1.x500.X500Name import java.util.* import javax.persistence.Tuple diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt index c325c673b4..fe91e3f579 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt @@ -19,7 +19,6 @@ import net.corda.core.serialization.storageKryo import net.corda.core.utilities.debug import net.corda.core.utilities.loggerFor import net.corda.node.services.database.HibernateConfiguration -import net.corda.node.services.vault.schemas.jpa.VaultSchemaV1 import org.jetbrains.exposed.sql.transactions.TransactionManager import rx.subjects.PublishSubject import java.lang.Exception diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index 597e9f2595..b93d90137c 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -21,8 +21,14 @@ import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.messaging.DataFeed import net.corda.core.node.ServiceHub -import net.corda.core.node.services.* -import net.corda.core.serialization.* +import net.corda.core.node.services.StatesNotAvailableException +import net.corda.core.node.services.Vault +import net.corda.core.node.services.VaultService +import net.corda.core.node.services.unconsumedStates +import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.serialize +import net.corda.core.serialization.storageKryo import net.corda.core.tee import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction @@ -33,6 +39,7 @@ import net.corda.core.utilities.trace import net.corda.node.services.database.RequeryConfiguration import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.services.vault.schemas.requery.* +import net.corda.node.services.vault.schemas.requery.VaultSchema import net.corda.node.utilities.bufferUntilDatabaseCommit import net.corda.node.utilities.wrapWithDatabaseTransaction import rx.Observable diff --git a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt index 316ea64e03..5ef516971a 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt @@ -1,8 +1,9 @@ -package net.corda.node.services.vault.schemas.jpa +package net.corda.node.services.vault import net.corda.core.contracts.UniqueIdentifier import net.corda.core.identity.AbstractParty import net.corda.core.node.services.Vault +import net.corda.core.schemas.CommonSchemaV1 import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState import net.corda.core.utilities.OpaqueBytes diff --git a/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt index 48e56ddc55..cc8c9a98b1 100644 --- a/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt @@ -24,8 +24,8 @@ import net.corda.testing.DUMMY_NOTARY import net.corda.node.services.schema.HibernateObserver import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.vault.NodeVaultService -import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1 -import net.corda.node.services.vault.schemas.jpa.VaultSchemaV1 +import net.corda.core.schemas.CommonSchemaV1 +import net.corda.node.services.vault.VaultSchemaV1 import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction import net.corda.schemas.CashSchemaV1 @@ -649,7 +649,7 @@ class HibernateConfigurationTest { // search predicate val cashStatesSchema = criteriaQuery.from(SampleCashSchemaV3.PersistentCashState::class.java) - val joinCashToParty = cashStatesSchema.join("owner") + val joinCashToParty = cashStatesSchema.join("owner") val queryOwnerKey = BOB_PUBKEY.toBase58String() criteriaQuery.where(criteriaBuilder.equal(joinCashToParty.get("key"), queryOwnerKey)) diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index b669b07f6d..587e95081a 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -19,7 +19,6 @@ import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.toHexString import net.corda.node.services.database.HibernateConfiguration import net.corda.node.services.schema.NodeSchemaService -import net.corda.node.services.vault.schemas.jpa.VaultSchemaV1 import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction import net.corda.schemas.CashSchemaV1 diff --git a/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyDealStateSchemaV1.kt b/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyDealStateSchemaV1.kt index b28e8cfe44..c59a09b192 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyDealStateSchemaV1.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyDealStateSchemaV1.kt @@ -1,8 +1,8 @@ package net.corda.testing.schemas import net.corda.core.contracts.UniqueIdentifier +import net.corda.core.schemas.CommonSchemaV1 import net.corda.core.schemas.MappedSchema -import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1 import javax.persistence.Column import javax.persistence.Entity import javax.persistence.Table diff --git a/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV2.kt b/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV2.kt index 91f3e49cbf..44b2df08e0 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV2.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV2.kt @@ -1,8 +1,8 @@ package net.corda.testing.schemas import net.corda.core.contracts.UniqueIdentifier +import net.corda.core.schemas.CommonSchemaV1 import net.corda.core.schemas.MappedSchema -import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1 import javax.persistence.Column import javax.persistence.Entity import javax.persistence.Table From f6aa672215bc65d26c9f29b3bce6759a97c1f04b Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Tue, 11 Jul 2017 14:21:37 +0100 Subject: [PATCH 94/97] Add unit tests around vault update derivation --- .../node/services/vault/NodeVaultService.kt | 7 +- .../services/vault/NodeVaultServiceTest.kt | 68 ++++++++++++++++--- 2 files changed, 64 insertions(+), 11 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index b93d90137c..2ba51cbf06 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -2,6 +2,7 @@ package net.corda.node.services.vault import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.strands.Strand +import com.google.common.annotations.VisibleForTesting import io.requery.PersistenceException import io.requery.TransactionIsolation import io.requery.kotlin.`in` @@ -468,7 +469,8 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P private fun deriveState(txState: TransactionState, amount: Amount>, owner: AbstractParty) = txState.copy(data = txState.data.copy(amount = amount, owner = owner)) - private fun makeUpdate(tx: WireTransaction, ourKeys: Set): Vault.Update { + @VisibleForTesting + internal fun makeUpdate(tx: WireTransaction, ourKeys: Set): Vault.Update { val ourNewStates = tx.outputs. filter { isRelevant(it.data, ourKeys) }. map { tx.outRef(it.data) } @@ -515,7 +517,8 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P authorisedUpgrade.remove(stateAndRef.ref) } - private fun isRelevant(state: ContractState, ourKeys: Set) = when (state) { + @VisibleForTesting + internal fun isRelevant(state: ContractState, ourKeys: Set) = when (state) { is OwnableState -> state.owner.owningKey.containsAny(ourKeys) is LinearState -> state.isRelevant(ourKeys) else -> ourKeys.intersect(state.participants.map { it.owningKey }).isNotEmpty() diff --git a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt index 1f52670c79..2e100edf28 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt @@ -2,22 +2,20 @@ package net.corda.node.services.vault import net.corda.contracts.asset.Cash import net.corda.contracts.asset.DUMMY_CASH_ISSUER -import net.corda.testing.contracts.fillWithSomeTestCash import net.corda.core.contracts.* +import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.AnonymousParty import net.corda.core.node.services.StatesNotAvailableException +import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultService import net.corda.core.node.services.unconsumedStates -import net.corda.core.utilities.OpaqueBytes import net.corda.core.transactions.SignedTransaction -import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.LogHelper +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.OpaqueBytes import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction -import net.corda.testing.BOC -import net.corda.testing.BOC_KEY -import net.corda.testing.MEGA_CORP -import net.corda.testing.MEGA_CORP_KEY +import net.corda.testing.* +import net.corda.testing.contracts.fillWithSomeTestCash import net.corda.testing.node.MockServices import net.corda.testing.node.makeTestDataSourceProperties import org.assertj.core.api.Assertions.assertThat @@ -31,7 +29,9 @@ import java.util.* import java.util.concurrent.CountDownLatch import java.util.concurrent.Executors import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertNull +import kotlin.test.assertTrue class NodeVaultServiceTest { lateinit var services: MockServices @@ -420,8 +420,58 @@ class NodeVaultServiceTest { services.recordTransactions(anotherTX) - vaultSvc.addNoteToTransaction(anotherTX.id, "GPB Sample Note 1") + vaultSvc.addNoteToTransaction(anotherTX.id, "GBP Sample Note 1") assertEquals(1, vaultSvc.getTransactionNotes(anotherTX.id).count()) } } + + @Test + fun `is ownable state relevant`() { + val service = (services.vaultService as NodeVaultService) + val amount = Amount(1000, Issued(BOC.ref(1), GBP)) + val wellKnownCash = Cash.State(amount, services.myInfo.legalIdentity) + assertTrue { service.isRelevant(wellKnownCash, services.keyManagementService.keys) } + + val anonymousIdentity = services.keyManagementService.freshKeyAndCert(services.myInfo.legalIdentityAndCert, false) + val anonymousCash = Cash.State(amount, anonymousIdentity.identity) + assertTrue { service.isRelevant(anonymousCash, services.keyManagementService.keys) } + + val thirdPartyIdentity = AnonymousParty(generateKeyPair().public) + val thirdPartyCash = Cash.State(amount, thirdPartyIdentity) + assertFalse { service.isRelevant(thirdPartyCash, services.keyManagementService.keys) } + } + + // TODO: Unit test linear state relevancy checks + + @Test + fun `make update`() { + val service = (services.vaultService as NodeVaultService) + val anonymousIdentity = services.keyManagementService.freshKeyAndCert(services.myInfo.legalIdentityAndCert, false) + val thirdPartyIdentity = AnonymousParty(generateKeyPair().public) + val amount = Amount(1000, Issued(BOC.ref(1), GBP)) + + // Issue then move some cash + val issueTx = TransactionBuilder(TransactionType.General, services.myInfo.legalIdentity).apply { + Cash().generateIssue(this, + amount, anonymousIdentity.identity, services.myInfo.legalIdentity) + }.toWireTransaction() + val cashState = StateAndRef(issueTx.outputs.single(), StateRef(issueTx.id, 0)) + + database.transaction { + val expected = Vault.Update(emptySet(), setOf(cashState), null) + val actual = service.makeUpdate(issueTx, setOf(anonymousIdentity.identity.owningKey)) + assertEquals(expected, actual) + services.vaultService.notify(issueTx) + } + + database.transaction { + val moveTx = TransactionBuilder(TransactionType.General, services.myInfo.legalIdentity).apply { + services.vaultService.generateSpend(this, Amount(1000, GBP), thirdPartyIdentity) + }.toWireTransaction() + + val expected = Vault.Update(setOf(cashState), emptySet(), null) + val actual = service.makeUpdate(moveTx, setOf(anonymousIdentity.identity.owningKey)) + assertEquals(expected, actual) + } + } } From 773aa28873822a0a36932d4b62f68927b4eaf985 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Wed, 12 Jul 2017 15:31:33 +0100 Subject: [PATCH 95/97] Clean up IssuerFlow * Switch to using anonymous party as recipient * Enable anonymisation for issuance as well as move in issuer flows. * Pass notary into issuer flow rather than taking a notary at random from the network map. * Enable anonymisation in Bank of Corda RPC test * Parameterize issuer flow tests into anonymous and deanonymised versions --- .../net/corda/core/messaging/CordaRPCOps.kt | 19 +++++++++ .../main/kotlin/net/corda/flows/IssuerFlow.kt | 11 ++--- .../net/corda/flows/TwoPartyTradeFlow.kt | 11 ++--- .../kotlin/net/corda/flows/IssuerFlowTest.kt | 41 +++++++++++++------ .../corda/node/internal/CordaRPCOpsImpl.kt | 2 + .../node/messaging/TwoPartyTradeFlowTests.kt | 3 +- .../net/corda/bank/BankOfCordaHttpAPITest.kt | 6 +-- .../corda/bank/BankOfCordaRPCClientTest.kt | 4 +- .../net/corda/bank/BankOfCordaDriver.kt | 2 +- .../corda/bank/api/BankOfCordaClientApi.kt | 5 ++- .../net/corda/bank/api/BankOfCordaWebApi.kt | 20 +++++---- .../corda/traderdemo/TraderDemoClientApi.kt | 9 +++- .../net/corda/traderdemo/flow/SellerFlow.kt | 5 ++- .../views/cordapps/cash/NewTransaction.kt | 1 + 14 files changed, 95 insertions(+), 44 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt index abf2e5d4d6..bf99ebe570 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt @@ -9,6 +9,7 @@ import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowInitiator import net.corda.core.flows.FlowLogic import net.corda.core.flows.StateMachineRunId +import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.node.NodeInfo import net.corda.core.node.services.NetworkMapCache @@ -303,6 +304,13 @@ interface CordaRPCOps : RPCOps { /** Enumerates the class names of the flows that this node knows about. */ fun registeredFlows(): List + + /** + * Returns a node's identity from the network map cache, where known. + * + * @return the node info if available. + */ + fun nodeIdentityFromParty(party: AbstractParty): NodeInfo? } inline fun CordaRPCOps.vaultQueryBy(criteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(), @@ -371,6 +379,17 @@ inline fun > CordaRPCOps.startF arg4: E ): FlowHandle = startFlowDynamic(R::class.java, arg0, arg1, arg2, arg3, arg4) +inline fun > CordaRPCOps.startFlow( + @Suppress("UNUSED_PARAMETER") + flowConstructor: (A, B, C, D, E, F) -> R, + arg0: A, + arg1: B, + arg2: C, + arg3: D, + arg4: E, + arg5: F +): FlowHandle = startFlowDynamic(R::class.java, arg0, arg1, arg2, arg3, arg4, arg5) + /** * Same again, except this time with progress-tracking enabled. */ diff --git a/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt b/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt index 3b4771a652..72d11fe0c7 100644 --- a/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt @@ -24,6 +24,7 @@ object IssuerFlow { data class IssuanceRequestState(val amount: Amount, val issueToParty: Party, val issuerPartyRef: OpaqueBytes, + val notaryParty: Party, val anonymous: Boolean) /** @@ -39,11 +40,12 @@ object IssuerFlow { val issueToParty: Party, val issueToPartyRef: OpaqueBytes, val issuerBankParty: Party, + val notaryParty: Party, val anonymous: Boolean) : FlowLogic() { @Suspendable @Throws(CashException::class) override fun call(): AbstractCashFlow.Result { - val issueRequest = IssuanceRequestState(amount, issueToParty, issueToPartyRef, anonymous) + val issueRequest = IssuanceRequestState(amount, issueToParty, issueToPartyRef, notaryParty, anonymous) return sendAndReceive(issuerBankParty, issueRequest).unwrap { res -> val tx = res.stx.tx val expectedAmount = Amount(amount.quantity, Issued(issuerBankParty.ref(issueToPartyRef), amount.token)) @@ -86,7 +88,7 @@ object IssuerFlow { it } // TODO: parse request to determine Asset to issue - val txn = issueCashTo(issueRequest.amount, issueRequest.issueToParty, issueRequest.issuerPartyRef, issueRequest.anonymous) + val txn = issueCashTo(issueRequest.amount, issueRequest.issueToParty, issueRequest.issuerPartyRef, issueRequest.notaryParty, issueRequest.anonymous) progressTracker.currentStep = SENDING_CONFIRM send(otherParty, txn) return txn.stx @@ -96,13 +98,12 @@ object IssuerFlow { private fun issueCashTo(amount: Amount, issueTo: Party, issuerPartyRef: OpaqueBytes, + notaryParty: Party, anonymous: Boolean): AbstractCashFlow.Result { - // TODO: pass notary in as request parameter - val notaryParty = serviceHub.networkMapCache.notaryNodes[0].notaryIdentity // invoke Cash subflow to issue Asset progressTracker.currentStep = ISSUING val issueRecipient = serviceHub.myInfo.legalIdentity - val issueCashFlow = CashIssueFlow(amount, issuerPartyRef, issueRecipient, notaryParty, anonymous = false) + val issueCashFlow = CashIssueFlow(amount, issuerPartyRef, issueRecipient, notaryParty, anonymous) val issueTx = subFlow(issueCashFlow) // NOTE: issueCashFlow performs a Broadcast (which stores a local copy of the txn to the ledger) // short-circuit when issuing to self diff --git a/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt b/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt index ebf7c704a3..d384e1255e 100644 --- a/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt @@ -5,6 +5,7 @@ import net.corda.contracts.asset.sumCashBy import net.corda.core.contracts.* import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic +import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.node.NodeInfo @@ -49,14 +50,14 @@ object TwoPartyTradeFlow { data class SellerTradeInfo( val assetForSale: StateAndRef, val price: Amount, - val sellerOwnerKey: PublicKey + val sellerOwner: AbstractParty ) open class Seller(val otherParty: Party, val notaryNode: NodeInfo, val assetToSell: StateAndRef, val price: Amount, - val myKey: PublicKey, + val me: AbstractParty, override val progressTracker: ProgressTracker = Seller.tracker()) : FlowLogic() { companion object { @@ -75,7 +76,7 @@ object TwoPartyTradeFlow { override fun call(): SignedTransaction { progressTracker.currentStep = AWAITING_PROPOSAL // Make the first message we'll send to kick off the flow. - val hello = SellerTradeInfo(assetToSell, price, myKey) + val hello = SellerTradeInfo(assetToSell, price, me) // What we get back from the other side is a transaction that *might* be valid and acceptable to us, // but we must check it out thoroughly before we sign! send(otherParty, hello) @@ -85,7 +86,7 @@ object TwoPartyTradeFlow { // DOCSTART 5 val signTransactionFlow = object : SignTransactionFlow(otherParty, VERIFYING_AND_SIGNING.childProgressTracker()) { override fun checkTransaction(stx: SignedTransaction) { - if (stx.tx.outputs.map { it.data }.sumCashBy(AnonymousParty(myKey)).withoutIssuer() != price) + if (stx.tx.outputs.map { it.data }.sumCashBy(me).withoutIssuer() != price) throw FlowException("Transaction is not sending us the right amount of cash") } } @@ -181,7 +182,7 @@ object TwoPartyTradeFlow { val ptx = TransactionType.General.Builder(notary) // Add input and output states for the movement of cash, by using the Cash contract to generate the states - val (tx, cashSigningPubKeys) = serviceHub.vaultService.generateSpend(ptx, tradeRequest.price, AnonymousParty(tradeRequest.sellerOwnerKey)) + val (tx, cashSigningPubKeys) = serviceHub.vaultService.generateSpend(ptx, tradeRequest.price, tradeRequest.sellerOwner) // Add inputs/outputs/a command for the movement of the asset. tx.addInputState(tradeRequest.assetForSale) diff --git a/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt b/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt index 3007a13a5c..bc98ce9943 100644 --- a/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt +++ b/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt @@ -22,10 +22,21 @@ import net.corda.testing.node.MockNetwork.MockNode import org.junit.After import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized import java.util.* import kotlin.test.assertFailsWith -class IssuerFlowTest { +@RunWith(Parameterized::class) +class IssuerFlowTest(val anonymous: Boolean) { + companion object { + @Parameterized.Parameters + @JvmStatic + fun data(): Collection> { + return listOf(arrayOf(false), arrayOf(true)) + } + } + lateinit var mockNet: MockNetwork lateinit var notaryNode: MockNode lateinit var bankOfCordaNode: MockNode @@ -46,6 +57,7 @@ class IssuerFlowTest { @Test fun `test issuer flow`() { + val notary = notaryNode.services.myInfo.notaryIdentity val (vaultUpdatesBoc, vaultUpdatesBankClient) = bankOfCordaNode.database.transaction { // Register for vault updates val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL) @@ -54,7 +66,7 @@ class IssuerFlowTest { // using default IssueTo Party Reference val issuerResult = runIssuerAndIssueRequester(bankOfCordaNode, bankClientNode, 1000000.DOLLARS, - bankClientNode.info.legalIdentity, OpaqueBytes.of(123)) + bankClientNode.info.legalIdentity, OpaqueBytes.of(123), notary) issuerResult.get() Pair(vaultUpdatesBoc, vaultUpdatesBankClient) @@ -68,8 +80,7 @@ class IssuerFlowTest { require(update.consumed.isEmpty()) { "Expected 0 consumed states, actual: $update" } require(update.produced.size == 1) { "Expected 1 produced states, actual: $update" } val issued = update.produced.single().state.data as Cash.State - require(issued.owner == bankOfCordaNode.info.legalIdentity) - require(issued.owner != bankClientNode.info.legalIdentity) + require(issued.owner.owningKey in bankOfCordaNode.services.keyManagementService.keys) }, // MOVE expect { update -> @@ -86,29 +97,31 @@ class IssuerFlowTest { require(update.consumed.isEmpty()) { update.consumed.size } require(update.produced.size == 1) { update.produced.size } val paidState = update.produced.single().state.data as Cash.State - require(paidState.owner == bankClientNode.info.legalIdentity) + require(paidState.owner.owningKey in bankClientNode.services.keyManagementService.keys) } } } @Test fun `test issuer flow rejects restricted`() { + val notary = notaryNode.services.myInfo.notaryIdentity // try to issue an amount of a restricted currency assertFailsWith { runIssuerAndIssueRequester(bankOfCordaNode, bankClientNode, Amount(100000L, currency("BRL")), - bankClientNode.info.legalIdentity, OpaqueBytes.of(123)).getOrThrow() + bankClientNode.info.legalIdentity, OpaqueBytes.of(123), notary).getOrThrow() } } @Test fun `test issue flow to self`() { + val notary = notaryNode.services.myInfo.notaryIdentity val vaultUpdatesBoc = bankOfCordaNode.database.transaction { val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL) val (_, vaultUpdatesBoc) = bankOfCordaNode.services.vaultQueryService.trackBy(criteria) // using default IssueTo Party Reference runIssuerAndIssueRequester(bankOfCordaNode, bankOfCordaNode, 1000000.DOLLARS, - bankOfCordaNode.info.legalIdentity, OpaqueBytes.of(123)).getOrThrow() + bankOfCordaNode.info.legalIdentity, OpaqueBytes.of(123), notary).getOrThrow() vaultUpdatesBoc } @@ -126,12 +139,13 @@ class IssuerFlowTest { @Test fun `test concurrent issuer flow`() { + val notary = notaryNode.services.myInfo.notaryIdentity // this test exercises the Cashflow issue and move subflows to ensure consistent spending of issued states val amount = 10000.DOLLARS val amounts = calculateRandomlySizedAmounts(10000.DOLLARS, 10, 10, Random()) val handles = amounts.map { pennies -> runIssuerAndIssueRequester(bankOfCordaNode, bankClientNode, Amount(pennies, amount.token), - bankClientNode.info.legalIdentity, OpaqueBytes.of(123)) + bankClientNode.info.legalIdentity, OpaqueBytes.of(123), notary) } handles.forEach { require(it.get().stx is SignedTransaction) @@ -141,11 +155,12 @@ class IssuerFlowTest { private fun runIssuerAndIssueRequester(issuerNode: MockNode, issueToNode: MockNode, amount: Amount, - party: Party, - ref: OpaqueBytes): ListenableFuture { - val issueToPartyAndRef = party.ref(ref) - val issueRequest = IssuanceRequester(amount, party, issueToPartyAndRef.reference, issuerNode.info.legalIdentity, - anonymous = false) + issueToParty: Party, + ref: OpaqueBytes, + notaryParty: Party): ListenableFuture { + val issueToPartyAndRef = issueToParty.ref(ref) + val issueRequest = IssuanceRequester(amount, issueToParty, issueToPartyAndRef.reference, issuerNode.info.legalIdentity, notaryParty, + anonymous) return issueToNode.services.startFlow(issueRequest).resultFuture } } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt index cec3445f1a..f881064e27 100644 --- a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt @@ -8,6 +8,7 @@ import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowInitiator import net.corda.core.flows.FlowLogic import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.messaging.* import net.corda.core.node.NodeInfo @@ -178,6 +179,7 @@ class CordaRPCOpsImpl( override fun partyFromName(name: String) = services.identityService.partyFromName(name) override fun partyFromX500Name(x500Name: X500Name) = services.identityService.partyFromX500Name(x500Name) override fun partiesFromName(query: String, exactMatch: Boolean): Set = services.identityService.partiesFromName(query, exactMatch) + override fun nodeIdentityFromParty(party: AbstractParty): NodeInfo? = services.networkMapCache.getNodeByLegalIdentity(party) override fun registeredFlows(): List = services.rpcFlows.map { it.name }.sorted() diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index d50a09fcd3..049e44a463 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -509,12 +509,13 @@ class TwoPartyTradeFlowTests { @Suspendable override fun call(): SignedTransaction { send(buyer, Pair(notary.notaryIdentity, price)) + val key = serviceHub.keyManagementService.freshKey() return subFlow(Seller( buyer, notary, assetToSell, price, - serviceHub.legalIdentityKey)) + AnonymousParty(key))) } } diff --git a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaHttpAPITest.kt b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaHttpAPITest.kt index 6305f03104..afee0671f7 100644 --- a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaHttpAPITest.kt +++ b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaHttpAPITest.kt @@ -5,9 +5,9 @@ import net.corda.bank.api.BankOfCordaClientApi import net.corda.bank.api.BankOfCordaWebApi.IssueRequestParams import net.corda.core.getOrThrow import net.corda.core.node.services.ServiceInfo -import net.corda.testing.driver.driver import net.corda.node.services.transactions.SimpleNotaryService import net.corda.testing.BOC +import net.corda.testing.driver.driver import org.junit.Test import kotlin.test.assertTrue @@ -19,9 +19,9 @@ class BankOfCordaHttpAPITest { startNode(BOC.name, setOf(ServiceInfo(SimpleNotaryService.type))), startNode(BIGCORP_LEGAL_NAME) ).getOrThrow() - val anonymous = true + val anonymous = false val nodeBankOfCordaApiAddr = startWebserver(nodeBankOfCorda).getOrThrow().listenAddress - assertTrue(BankOfCordaClientApi(nodeBankOfCordaApiAddr).requestWebIssue(IssueRequestParams(1000, "USD", BIGCORP_LEGAL_NAME, "1", BOC.name, anonymous))) + assertTrue(BankOfCordaClientApi(nodeBankOfCordaApiAddr).requestWebIssue(IssueRequestParams(1000, "USD", BIGCORP_LEGAL_NAME, "1", BOC.name, BOC.name, anonymous))) }, isDebug = true) } } diff --git a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt index 8c2ec6d765..7159c077b4 100644 --- a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt +++ b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt @@ -44,14 +44,14 @@ class BankOfCordaRPCClientTest { val (_, vaultUpdatesBigCorp) = bigCorpProxy.vaultTrackByCriteria(Cash.State::class.java, criteria) // Kick-off actual Issuer Flow - // TODO: Update checks below to reflect states consumed/produced under anonymisation - val anonymous = false + val anonymous = true bocProxy.startFlow( ::IssuanceRequester, 1000.DOLLARS, nodeBigCorporation.nodeInfo.legalIdentity, BIG_CORP_PARTY_REF, nodeBankOfCorda.nodeInfo.legalIdentity, + nodeBankOfCorda.nodeInfo.notaryIdentity, anonymous).returnValue.getOrThrow() // Check Bank of Corda Vault Updates diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt index 1517af2424..ed44a63309 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt @@ -68,7 +68,7 @@ private class BankOfCordaDriver { } else { try { val anonymous = true - val requestParams = IssueRequestParams(options.valueOf(quantity), options.valueOf(currency), BIGCORP_LEGAL_NAME, "1", BOC.name, anonymous) + val requestParams = IssueRequestParams(options.valueOf(quantity), options.valueOf(currency), BIGCORP_LEGAL_NAME, "1", BOC.name, DUMMY_NOTARY.name, anonymous) when (role) { Role.ISSUE_CASH_RPC -> { println("Requesting Cash via RPC ...") diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt index f8bf5df778..2f4da02b39 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt @@ -40,11 +40,14 @@ class BankOfCordaClientApi(val hostAndPort: NetworkHostAndPort) { ?: throw Exception("Unable to locate ${params.issueToPartyName} in Network Map Service") val issuerBankParty = proxy.partyFromX500Name(params.issuerBankName) ?: throw Exception("Unable to locate ${params.issuerBankName} in Network Map Service") + val notaryParty = proxy.partyFromX500Name(params.notaryName) + ?: throw Exception("Unable to locate ${params.notaryName} in Network Map Service") val amount = Amount(params.amount, currency(params.currency)) val issuerToPartyRef = OpaqueBytes.of(params.issueToPartyRefAsString.toByte()) - return proxy.startFlow(::IssuanceRequester, amount, issueToParty, issuerToPartyRef, issuerBankParty, params.anonymous).returnValue.getOrThrow().stx + return proxy.startFlow(::IssuanceRequester, amount, issueToParty, issuerToPartyRef, issuerBankParty, notaryParty, params.anonymous) + .returnValue.getOrThrow().stx } } } diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt index 1b74f2e0d2..31c8b3abf3 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt @@ -2,7 +2,6 @@ package net.corda.bank.api import net.corda.core.contracts.Amount import net.corda.core.contracts.currency -import net.corda.core.flows.FlowException import net.corda.core.getOrThrow import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.startFlow @@ -21,6 +20,7 @@ class BankOfCordaWebApi(val rpc: CordaRPCOps) { data class IssueRequestParams(val amount: Long, val currency: String, val issueToPartyName: X500Name, val issueToPartyRefAsString: String, val issuerBankName: X500Name, + val notaryName: X500Name, val anonymous: Boolean) private companion object { @@ -43,9 +43,11 @@ class BankOfCordaWebApi(val rpc: CordaRPCOps) { fun issueAssetRequest(params: IssueRequestParams): Response { // Resolve parties via RPC val issueToParty = rpc.partyFromX500Name(params.issueToPartyName) - ?: throw Exception("Unable to locate ${params.issueToPartyName} in Network Map Service") + ?: return Response.status(Response.Status.FORBIDDEN).entity("Unable to locate ${params.issueToPartyName} in Network Map Service").build() val issuerBankParty = rpc.partyFromX500Name(params.issuerBankName) - ?: throw Exception("Unable to locate ${params.issuerBankName} in Network Map Service") + ?: return Response.status(Response.Status.FORBIDDEN).entity("Unable to locate ${params.issuerBankName} in Network Map Service").build() + val notaryParty = rpc.partyFromX500Name(params.notaryName) + ?: return Response.status(Response.Status.FORBIDDEN).entity("Unable to locate ${params.notaryName} in Network Map Service").build() val amount = Amount(params.amount, currency(params.currency)) val issuerToPartyRef = OpaqueBytes.of(params.issueToPartyRefAsString.toByte()) @@ -53,13 +55,13 @@ class BankOfCordaWebApi(val rpc: CordaRPCOps) { // invoke client side of Issuer Flow: IssuanceRequester // The line below blocks and waits for the future to resolve. - val status = try { - rpc.startFlow(::IssuanceRequester, amount, issueToParty, issuerToPartyRef, issuerBankParty, anonymous).returnValue.getOrThrow() + return try { + rpc.startFlow(::IssuanceRequester, amount, issueToParty, issuerToPartyRef, issuerBankParty, notaryParty, anonymous).returnValue.getOrThrow() logger.info("Issue request completed successfully: $params") - Response.Status.CREATED - } catch (e: FlowException) { - Response.Status.BAD_REQUEST + Response.status(Response.Status.CREATED).build() + } catch (e: Exception) { + logger.error("Issue request failed: ${e}", e) + Response.status(Response.Status.FORBIDDEN).build() } - return Response.status(status).build() } } \ No newline at end of file diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt index 434f652257..7f2ddbee4e 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt @@ -17,6 +17,7 @@ import net.corda.core.utilities.Emoji import net.corda.core.utilities.loggerFor import net.corda.flows.IssuerFlow.IssuanceRequester import net.corda.testing.BOC +import net.corda.testing.DUMMY_NOTARY import net.corda.traderdemo.flow.SellerFlow import org.bouncycastle.asn1.x500.X500Name import java.util.* @@ -45,12 +46,16 @@ class TraderDemoClientApi(val rpc: CordaRPCOps) { fun runBuyer(amount: Amount = 30000.DOLLARS, anonymous: Boolean = true) { val bankOfCordaParty = rpc.partyFromX500Name(BOC.name) - ?: throw Exception("Unable to locate ${BOC.name} in Network Map Service") + ?: throw IllegalStateException("Unable to locate ${BOC.name} in Network Map Service") + val notaryLegalIdentity = rpc.partyFromX500Name(DUMMY_NOTARY.name) + ?: throw IllegalStateException("Unable to locate ${DUMMY_NOTARY.name} in Network Map Service") + val notaryNode = rpc.nodeIdentityFromParty(notaryLegalIdentity) + ?: throw IllegalStateException("Unable to locate notary node in network map cache") val me = rpc.nodeIdentity() val amounts = calculateRandomlySizedAmounts(amount, 3, 10, Random()) // issuer random amounts of currency totaling 30000.DOLLARS in parallel val resultFutures = amounts.map { pennies -> - rpc.startFlow(::IssuanceRequester, Amount(pennies, amount.token), me.legalIdentity, OpaqueBytes.of(1), bankOfCordaParty, anonymous).returnValue + rpc.startFlow(::IssuanceRequester, Amount(pennies, amount.token), me.legalIdentity, OpaqueBytes.of(1), bankOfCordaParty, notaryNode.notaryIdentity, anonymous).returnValue } Futures.allAsList(resultFutures).getOrThrow() diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt index e46b4073ec..720f33ffe5 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt @@ -11,6 +11,7 @@ import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.StartableByRPC import net.corda.core.identity.AbstractParty +import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.node.NodeInfo import net.corda.core.seconds @@ -50,7 +51,7 @@ class SellerFlow(val otherParty: Party, progressTracker.currentStep = SELF_ISSUING val notary: NodeInfo = serviceHub.networkMapCache.notaryNodes[0] - val cpOwnerKey = serviceHub.legalIdentityKey + val cpOwnerKey = serviceHub.keyManagementService.freshKey() val commercialPaper = selfIssueSomeCommercialPaper(serviceHub.myInfo.legalIdentity, notary) progressTracker.currentStep = TRADING @@ -62,7 +63,7 @@ class SellerFlow(val otherParty: Party, notary, commercialPaper, amount, - cpOwnerKey, + AnonymousParty(cpOwnerKey), progressTracker.getChildProgressTracker(TRADING)!!) return subFlow(seller) } diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt index 087615728f..d2a59c1cea 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt @@ -100,6 +100,7 @@ class NewTransaction : Fragment() { command.recipient, command.issueRef, myIdentity.value!!.legalIdentity, + command.notary, command.anonymous) } else { command.startFlow(rpcProxy.value!!) From ce06ad38784c0130cccdc4d7ca36a743e09607cf Mon Sep 17 00:00:00 2001 From: Konstantinos Chalkias Date: Thu, 13 Jul 2017 11:24:33 +0100 Subject: [PATCH 96/97] Remove DigitalSignature.LegallyIdentifiable Remove DigitialSignature.LegallyIdentifiable --- .../main/kotlin/net/corda/core/crypto/CryptoUtils.kt | 12 ------------ .../kotlin/net/corda/core/crypto/DigitalSignature.kt | 4 ---- docs/source/oracles.rst | 2 +- .../services/keys/PersistentKeyManagementService.kt | 2 -- .../kotlin/net/corda/irs/api/NodeInterestRates.kt | 4 ++-- .../main/kotlin/net/corda/irs/flows/RatesFixFlow.kt | 9 +++++---- 6 files changed, 8 insertions(+), 25 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt index 4bd951edaf..c1e390a84c 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt @@ -3,7 +3,6 @@ package net.corda.core.crypto import net.corda.core.crypto.composite.CompositeKey -import net.corda.core.identity.Party import net.corda.core.utilities.OpaqueBytes import java.math.BigInteger import java.security.* @@ -36,17 +35,6 @@ fun PrivateKey.sign(bytesToSign: ByteArray, publicKey: PublicKey): DigitalSignat @Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class) fun KeyPair.sign(bytesToSign: ByteArray) = private.sign(bytesToSign, public) fun KeyPair.sign(bytesToSign: OpaqueBytes) = private.sign(bytesToSign.bytes, public) -fun KeyPair.sign(bytesToSign: OpaqueBytes, party: Party) = sign(bytesToSign.bytes, party) - -// TODO This case will need more careful thinking, as party owningKey can be a CompositeKey. One way of doing that is -// implementation of CompositeSignature. -@Throws(InvalidKeyException::class) -fun KeyPair.sign(bytesToSign: ByteArray, party: Party): DigitalSignature.LegallyIdentifiable { - // Quick workaround when we have CompositeKey as Party owningKey. - if (party.owningKey is CompositeKey) throw InvalidKeyException("Signing for parties with CompositeKey not supported.") - val sig = sign(bytesToSign) - return DigitalSignature.LegallyIdentifiable(party, sig.bytes) -} /** * Utility to simplify the act of verifying a signature. diff --git a/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt b/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt index 738f1e108b..db7cf6473a 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt @@ -1,6 +1,5 @@ package net.corda.core.crypto -import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.OpaqueBytes import java.security.InvalidKeyException @@ -46,7 +45,4 @@ open class DigitalSignature(bits: ByteArray) : OpaqueBytes(bits) { @Throws(InvalidKeyException::class, SignatureException::class) fun isValid(content: ByteArray) = by.isValid(content, this) } - - // TODO: consider removing this as whoever needs to identify the signer should be able to derive it from the public key - class LegallyIdentifiable(val signer: Party, bits: ByteArray) : WithKey(signer.owningKey, bits) } diff --git a/docs/source/oracles.rst b/docs/source/oracles.rst index e77147893a..921562b671 100644 --- a/docs/source/oracles.rst +++ b/docs/source/oracles.rst @@ -112,7 +112,7 @@ Here is an extract from the ``NodeInterestRates.Oracle`` class and supporting ty class Oracle { fun query(queries: List, deadline: Instant): List - fun sign(ftx: FilteredTransaction, merkleRoot: SecureHash): DigitalSignature.LegallyIdentifiable + fun sign(ftx: FilteredTransaction, merkleRoot: SecureHash): DigitalSignature.WithKey } Because the fix contains a timestamp (the ``forDay`` field), that identifies the version of the data being requested, diff --git a/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt b/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt index 1a9b5be81e..359239bc0f 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt @@ -11,14 +11,12 @@ import net.corda.core.node.services.KeyManagementService import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.flows.AnonymisedIdentity import net.corda.node.utilities.* -import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.operator.ContentSigner import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.statements.InsertStatement import java.security.KeyPair import java.security.PrivateKey import java.security.PublicKey -import java.security.cert.CertPath /** * A persistent re-implementation of [E2ETestKeyManagementService] to support node re-start. diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt index e7c0b5ba8c..33604a7454 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt @@ -146,7 +146,7 @@ object NodeInterestRates { // Oracle gets signing request for only some of them with a valid partial tree? We sign over a whole transaction. // It will be fixed by adding partial signatures later. // DOCSTART 1 - fun sign(ftx: FilteredTransaction): DigitalSignature.LegallyIdentifiable { + fun sign(ftx: FilteredTransaction): DigitalSignature.WithKey { if (!ftx.verify()) { throw MerkleTreeException("Rate Fix Oracle: Couldn't verify partial Merkle tree.") } @@ -178,7 +178,7 @@ object NodeInterestRates { // version so we can't resolve or check it ourselves. However, that doesn't matter much, as if we sign // an invalid transaction the signature is worthless. val signature = services.keyManagementService.sign(ftx.rootHash.bytes, signingKey) - return DigitalSignature.LegallyIdentifiable(identity, signature.bytes) + return DigitalSignature.WithKey(signingKey, signature.bytes) } // DOCEND 1 diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt index 2d5b7b0986..0ab867f889 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt @@ -4,6 +4,7 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.Fix import net.corda.contracts.FixOf import net.corda.core.crypto.DigitalSignature +import net.corda.core.crypto.isFulfilledBy import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow import net.corda.core.identity.Party @@ -111,12 +112,12 @@ open class RatesFixFlow(protected val tx: TransactionBuilder, @InitiatingFlow class FixSignFlow(val tx: TransactionBuilder, val oracle: Party, - val partialMerkleTx: FilteredTransaction) : FlowLogic() { + val partialMerkleTx: FilteredTransaction) : FlowLogic() { @Suspendable - override fun call(): DigitalSignature.LegallyIdentifiable { - val resp = sendAndReceive(oracle, SignRequest(partialMerkleTx)) + override fun call(): DigitalSignature.WithKey { + val resp = sendAndReceive(oracle, SignRequest(partialMerkleTx)) return resp.unwrap { sig -> - check(sig.signer == oracle) + check(oracle.owningKey.isFulfilledBy(listOf(sig.by))) tx.toWireTransaction().checkSignature(sig) sig } From 0ec6f31f9498fd70f20637a6a47d2bfa002b6d56 Mon Sep 17 00:00:00 2001 From: David Lee Date: Thu, 13 Jul 2017 11:43:41 +0100 Subject: [PATCH 97/97] Updated URL link to R3 trademark policy --- TRADEMARK | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TRADEMARK b/TRADEMARK index 21b2d63a2d..aa2799e5d3 100644 --- a/TRADEMARK +++ b/TRADEMARK @@ -1,4 +1,4 @@ Corda and the Corda logo are trademarks of R3 HoldCo LLC and its affiliates. All rights reserved. -For R3 HoldCo LLC's trademark and logo usage information, please consult our Trademark Usage Policy available at https://www.r3.com/trademark-usage-policy +For R3 HoldCo LLC's trademark and logo usage information, please consult our Trademark Usage Policy available at https://www.r3.com/trademark-policy