Merged in rnicoll-obligation-infrastructure (pull request #167)

Add obligation infrastructure
This commit is contained in:
Ross Nicoll 2016-06-20 16:09:02 +01:00
commit 39d60bc74b
7 changed files with 85 additions and 9 deletions

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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.

View File

@ -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>,

View File

@ -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>()
}
}

View File

@ -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
}
}

View File

@ -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)
}
}
}
/**