From af53a52b06c32188126ad051c3e3c0f3bb2ed980 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Fri, 27 May 2016 16:46:52 +0100 Subject: [PATCH 1/3] Add common Issue and Move commands * Add common Issue command to encourage presence of a nonce value when issuing state objects. * Add common Move command for contracts which support being moved in order to fulfil other contracts. --- .../kotlin/com/r3corda/contracts/cash/Cash.kt | 11 +++++++++-- .../com/r3corda/contracts/cash/FungibleAsset.kt | 4 ++-- .../com/r3corda/core/contracts/Structures.kt | 15 +++++++++++++++ 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/cash/Cash.kt b/contracts/src/main/kotlin/com/r3corda/contracts/cash/Cash.kt index 871972ced3..628ad6e2b5 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/cash/Cash.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/cash/Cash.kt @@ -69,13 +69,20 @@ class Cash : FungibleAsset() { // Just for grouping interface Commands : CommandData { - class Move() : TypeOnlyCommandData(), FungibleAsset.Commands.Move + /** + * A command stating that money has been moved, optionally to fulfil another contract. + * + * @param contractHash the hash of the contract this cash is settling, to ensure one cash contract cannot be + * used to settle multiple contracts. May be null, if this is not relevant to any other contract in the + * same transaction + */ + data class Move(override val contractHash: SecureHash? = null) : FungibleAsset.Commands.Move, Commands /** * Allows new cash states to be issued into existence: the nonce ("number used once") ensures the transaction * has a unique ID even when there are no inputs. */ - data class Issue(override val nonce: Long = newSecureRandom().nextLong()) : FungibleAsset.Commands.Issue + data class Issue(override val nonce: Long = newSecureRandom().nextLong()) : FungibleAsset.Commands.Issue, Commands /** * A command stating that money has been withdrawn from the shared ledger and is now accounted for diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAsset.kt b/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAsset.kt index a865e6bcd0..957fbb2ec2 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAsset.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAsset.kt @@ -41,13 +41,13 @@ abstract class FungibleAsset : Contract { // Just for grouping interface Commands : CommandData { - interface Move : Commands + interface Move : MoveCommand, Commands /** * Allows new asset states to be issued into existence: the nonce ("number used once") ensures the transaction * has a unique ID even when there are no inputs. */ - interface Issue : Commands { val nonce: Long } + interface Issue : IssueCommand, Commands /** * A command stating that money has been withdrawn from the shared ledger and is now accounted for diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt b/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt index a6e69824fd..eb63e738fc 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt @@ -228,6 +228,21 @@ data class Command(val value: CommandData, val signers: List) { override fun toString() = "${commandDataToString()} with pubkeys ${signers.map { it.toStringShort() }}" } +/** A common issue command, to enforce that issue commands have a nonce value. */ +interface IssueCommand : CommandData { + val nonce: Long +} + +/** A common move command for contracts which can change owner. */ +interface MoveCommand : CommandData { + /** + * Contract code the moved state(s) are for the attention of, for example to indicate that the states are moved in + * order to settle an obligation contract's state object(s). + */ + // TODO: Replace SecureHash here with a general contract constraints object + val contractHash: SecureHash? +} + /** Wraps an object that was signed by a public key, which may be a well known/recognised institutional key. */ data class AuthenticatedObject( val signers: List, From 723e610dfc0f41d32d76bb22a84d9c23d7ce7979 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Fri, 27 May 2016 16:49:07 +0100 Subject: [PATCH 2/3] Rename verifyMoveCommand() Rename verifyMoveCommands() to verifyMoveCommand() to represent that move commands must be singular within a transaction. --- .../src/main/kotlin/com/r3corda/contracts/cash/FungibleAsset.kt | 2 +- core/src/main/kotlin/com/r3corda/core/contracts/ContractsDSL.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAsset.kt b/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAsset.kt index 957fbb2ec2..145ef9b0b8 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAsset.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAsset.kt @@ -89,7 +89,7 @@ abstract class FungibleAsset : Contract { (inputAmount == outputAmount + amountExitingLedger) } - verifyMoveCommands(inputs, tx) + verifyMoveCommand(inputs, tx) } } } diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/ContractsDSL.kt b/core/src/main/kotlin/com/r3corda/core/contracts/ContractsDSL.kt index d7018b9c3e..834fa36dda 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/ContractsDSL.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/ContractsDSL.kt @@ -92,7 +92,7 @@ fun List>.getTimestampByName(vararg names: Stri */ @Throws(IllegalArgumentException::class) // TODO: Can we have a common Move command for all contracts and avoid the reified type parameter here? -inline fun verifyMoveCommands(inputs: List, tx: TransactionForContract) { +inline fun verifyMoveCommand(inputs: List, tx: TransactionForContract) { // Now check the digital signatures on the move command. Every input has an owning public key, and we must // see a signature from each of those keys. The actual signatures have been verified against the transaction // data by the platform before execution. From 93e9d0459c240b854db6d77c78f06430b3900385 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Wed, 25 May 2016 12:55:19 +0100 Subject: [PATCH 3/3] Add custom serialiser for NonEmptySet --- .../com/r3corda/core/serialization/Kryo.kt | 5 +++ .../com/r3corda/core/utilities/NonEmptySet.kt | 36 +++++++++++++++++-- .../r3corda/core/utilities/NonEmptySetTest.kt | 19 +++++++++- 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/core/src/main/kotlin/com/r3corda/core/serialization/Kryo.kt b/core/src/main/kotlin/com/r3corda/core/serialization/Kryo.kt index e63962ac10..064cfda45b 100644 --- a/core/src/main/kotlin/com/r3corda/core/serialization/Kryo.kt +++ b/core/src/main/kotlin/com/r3corda/core/serialization/Kryo.kt @@ -16,6 +16,8 @@ import com.r3corda.core.crypto.generateKeyPair import com.r3corda.core.crypto.sha256 import com.r3corda.core.node.AttachmentsClassLoader import com.r3corda.core.node.services.AttachmentStorage +import com.r3corda.core.utilities.NonEmptySet +import com.r3corda.core.utilities.NonEmptySetSerializer import de.javakaffee.kryoserializers.ArraysAsListSerializer import net.i2p.crypto.eddsa.EdDSAPrivateKey import net.i2p.crypto.eddsa.EdDSAPublicKey @@ -350,6 +352,9 @@ fun createKryo(k: Kryo = Kryo()): Kryo { register(Issued::class.java, ImmutableClassSerializer(Issued::class)) register(TransactionState::class.java, ImmutableClassSerializer(TransactionState::class)) + // This ensures a NonEmptySetSerializer is constructed with an initial value. + register(NonEmptySet::class.java, NonEmptySetSerializer) + noReferencesWithin() } } diff --git a/core/src/main/kotlin/com/r3corda/core/utilities/NonEmptySet.kt b/core/src/main/kotlin/com/r3corda/core/utilities/NonEmptySet.kt index 89739639e5..a12b5c49c7 100644 --- a/core/src/main/kotlin/com/r3corda/core/utilities/NonEmptySet.kt +++ b/core/src/main/kotlin/com/r3corda/core/utilities/NonEmptySet.kt @@ -1,12 +1,20 @@ package com.r3corda.core.utilities +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.Serializer +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import java.util.* + /** * A set which is constrained to ensure it can never be empty. An initial value must be provided at * construction, and attempting to remove the last element will cause an IllegalStateException. + * The underlying set is exposed for Kryo to access, but should not be accessed directly. */ -class NonEmptySet(initial: T, private val set: MutableSet = mutableSetOf()) : MutableSet { +class NonEmptySet(initial: T) : MutableSet { + private val set: MutableSet = HashSet() + init { - require (set.isEmpty()) { "Provided set must be empty." } set.add(initial) } @@ -80,4 +88,28 @@ fun nonEmptySetOf(initial: T, vararg elements: T): NonEmptySet { // We add the first element twice, but it's a set, so who cares set.addAll(elements) return set +} + +/** + * Custom serializer which understands it has to read in an item before + * trying to construct the set. + */ +object NonEmptySetSerializer : Serializer>() { + override fun write(kryo: Kryo, output: Output, obj: NonEmptySet) { + // Write out the contents as normal + output.writeInt(obj.size) + obj.forEach { kryo.writeClassAndObject(output, it) } + } + + override fun read(kryo: Kryo, input: Input, type: Class>): NonEmptySet { + val size = input.readInt() + require(size >= 1) { "Size is positive" } + // TODO: Is there an upper limit we can apply to how big one of these could be? + val first = kryo.readClassAndObject(input) + // Read the first item and use it to construct the NonEmptySet + val set = NonEmptySet(first) + // Read in the rest of the set + for (i in 2..size) { set.add(kryo.readClassAndObject(input)) } + return set + } } \ No newline at end of file diff --git a/core/src/test/kotlin/com/r3corda/core/utilities/NonEmptySetTest.kt b/core/src/test/kotlin/com/r3corda/core/utilities/NonEmptySetTest.kt index 58f07eefab..eb0567730a 100644 --- a/core/src/test/kotlin/com/r3corda/core/utilities/NonEmptySetTest.kt +++ b/core/src/test/kotlin/com/r3corda/core/utilities/NonEmptySetTest.kt @@ -8,6 +8,8 @@ import com.google.common.collect.testing.testers.CollectionAddAllTester import com.google.common.collect.testing.testers.CollectionClearTester import com.google.common.collect.testing.testers.CollectionRemoveAllTester import com.google.common.collect.testing.testers.CollectionRetainAllTester +import com.r3corda.core.serialization.deserialize +import com.r3corda.core.serialization.serialize import junit.framework.TestSuite import org.junit.Test import org.junit.runner.RunWith @@ -17,7 +19,8 @@ import kotlin.test.assertEquals @RunWith(Suite::class) @Suite.SuiteClasses( NonEmptySetTest.Guava::class, - NonEmptySetTest.Remove::class + NonEmptySetTest.Remove::class, + NonEmptySetTest.Serializer::class ) class NonEmptySetTest { /** @@ -93,6 +96,20 @@ class NonEmptySetTest { } } } + + /** + * Test serialization/deserialization. + */ + class Serializer { + @Test + fun `serialize deserialize`() { + val expected: NonEmptySet = nonEmptySetOf(-17, 22, 17) + val serialized = expected.serialize().bits + val actual = serialized.deserialize>() + + assertEquals(expected, actual) + } + } } /**