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<Currency>() { // 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..145ef9b0b8 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<T> : 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 @@ -89,7 +89,7 @@ abstract class FungibleAsset<T> : Contract { (inputAmount == outputAmount + amountExitingLedger) } - verifyMoveCommands<Commands.Move>(inputs, tx) + verifyMoveCommand<Commands.Move>(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<AuthenticatedObject<CommandData>>.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 <reified T : CommandData> verifyMoveCommands(inputs: List<OwnableState>, tx: TransactionForContract) { +inline fun <reified T : CommandData> verifyMoveCommand(inputs: List<OwnableState>, 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. 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<PublicKey>) { 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<out T : Any>( val signers: List<PublicKey>, 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<WireTransaction>() } } 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<T>(initial: T, private val set: MutableSet<T> = mutableSetOf()) : MutableSet<T> { +class NonEmptySet<T>(initial: T) : MutableSet<T> { + private val set: MutableSet<T> = HashSet<T>() + init { - require (set.isEmpty()) { "Provided set must be empty." } set.add(initial) } @@ -80,4 +88,28 @@ fun <T> nonEmptySetOf(initial: T, vararg elements: T): NonEmptySet<T> { // 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<NonEmptySet<Any>>() { + override fun write(kryo: Kryo, output: Output, obj: NonEmptySet<Any>) { + // 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<Any>>): NonEmptySet<Any> { + 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<Int> = nonEmptySetOf(-17, 22, 17) + val serialized = expected.serialize().bits + val actual = serialized.deserialize<NonEmptySet<Int>>() + + assertEquals(expected, actual) + } + } } /**