From e5dbf5d2a8ca8a9f30d4e402a83561eeda1b483f Mon Sep 17 00:00:00 2001 From: sofusmortensen Date: Thu, 24 Mar 2016 12:06:41 +0000 Subject: [PATCH] WireTransaction deserialization using AttachmentStorage --- .../kotlin/contracts/AnotherDummyContract.kt | 21 +++++-- .../kotlin/core/node/DummyContractBackdoor.kt | 7 +++ .../main/kotlin/contracts/DummyContract.kt | 15 +++-- core/src/main/kotlin/core/Transactions.kt | 6 +- .../src}/main/kotlin/core/node/ClassLoader.kt | 0 .../kotlin/core/node/services/Services.kt | 33 +++++++++++ .../main/kotlin/core/serialization/Kryo.kt | 34 ++++++++--- .../kotlin/core/node/services/Services.kt | 27 --------- src/test/kotlin/core/node/ClassLoaderTests.kt | 58 +++++++++++++++++-- 9 files changed, 147 insertions(+), 54 deletions(-) create mode 100644 contracts/isolated/src/main/kotlin/core/node/DummyContractBackdoor.kt rename {src => core/src}/main/kotlin/core/node/ClassLoader.kt (100%) create mode 100644 core/src/main/kotlin/core/node/services/Services.kt diff --git a/contracts/isolated/src/main/kotlin/contracts/AnotherDummyContract.kt b/contracts/isolated/src/main/kotlin/contracts/AnotherDummyContract.kt index 1cee079cbb..bfad79a036 100644 --- a/contracts/isolated/src/main/kotlin/contracts/AnotherDummyContract.kt +++ b/contracts/isolated/src/main/kotlin/contracts/AnotherDummyContract.kt @@ -18,18 +18,27 @@ import core.crypto.SecureHash val ANOTHER_DUMMY_PROGRAM_ID = AnotherDummyContract() -class AnotherDummyContract : Contract { - data class State(val foo: Int) : ContractState { +class AnotherDummyContract : Contract, core.node.DummyContractBackdoor { + class State(val magicNumber: Int = 0) : ContractState { override val contract = ANOTHER_DUMMY_PROGRAM_ID } + interface Commands : CommandData { + class Create : TypeOnlyCommandData(), Commands + } + override fun verify(tx: TransactionForVerification) { - requireThat { - "justice will be served" by false - } // Always accepts. } // The "empty contract" - override val legalContractReference = SecureHash.sha256("https://anotherdummy.org") + override val legalContractReference: SecureHash = SecureHash.sha256("https://anotherdummy.org") + + override fun generateInitial(owner: PartyReference, magicNumber: Int) : TransactionBuilder { + val state = State(magicNumber) + return TransactionBuilder().withItems( state, Command(Commands.Create(), owner.party.owningKey) ) + } + + override fun inspectState(state: core.ContractState) : Int = (state as State).magicNumber + } \ No newline at end of file diff --git a/contracts/isolated/src/main/kotlin/core/node/DummyContractBackdoor.kt b/contracts/isolated/src/main/kotlin/core/node/DummyContractBackdoor.kt new file mode 100644 index 0000000000..4349a295c9 --- /dev/null +++ b/contracts/isolated/src/main/kotlin/core/node/DummyContractBackdoor.kt @@ -0,0 +1,7 @@ +package core.node + +interface DummyContractBackdoor { + fun generateInitial(owner: core.PartyReference, magicNumber: Int) : core.TransactionBuilder + + fun inspectState(state: core.ContractState) : Int +} \ No newline at end of file diff --git a/contracts/src/main/kotlin/contracts/DummyContract.kt b/contracts/src/main/kotlin/contracts/DummyContract.kt index 5a7761acba..2a4fab2ead 100644 --- a/contracts/src/main/kotlin/contracts/DummyContract.kt +++ b/contracts/src/main/kotlin/contracts/DummyContract.kt @@ -8,9 +8,7 @@ package contracts -import core.Contract -import core.ContractState -import core.TransactionForVerification +import core.* import core.crypto.SecureHash // The dummy contract doesn't do anything useful. It exists for testing purposes. @@ -18,14 +16,23 @@ import core.crypto.SecureHash val DUMMY_PROGRAM_ID = DummyContract() class DummyContract : Contract { - class State : ContractState { + class State(val magicNumber: Int = 0) : ContractState { override val contract = DUMMY_PROGRAM_ID } + interface Commands : CommandData { + class Create : TypeOnlyCommandData(), Commands + } + override fun verify(tx: TransactionForVerification) { // Always accepts. } // The "empty contract" override val legalContractReference: SecureHash = SecureHash.sha256("") + + fun generateInitial(owner: PartyReference, magicNumber: Int) : TransactionBuilder { + val state = State(magicNumber) + return TransactionBuilder().withItems( state, Command(Commands.Create(), owner.party.owningKey) ) + } } \ No newline at end of file diff --git a/core/src/main/kotlin/core/Transactions.kt b/core/src/main/kotlin/core/Transactions.kt index ddd08f6d78..2f2c3aca4e 100644 --- a/core/src/main/kotlin/core/Transactions.kt +++ b/core/src/main/kotlin/core/Transactions.kt @@ -9,6 +9,7 @@ package core import co.paralleluniverse.fibers.Suspendable +import com.esotericsoftware.kryo.Kryo import core.crypto.DigitalSignature import core.crypto.SecureHash import core.crypto.signWithECDSA @@ -16,6 +17,7 @@ import core.crypto.toStringShort import core.node.services.TimestamperService import core.node.services.TimestampingError import core.serialization.SerializedBytes +import core.serialization.THREAD_LOCAL_KRYO import core.serialization.deserialize import core.serialization.serialize import core.utilities.Emoji @@ -68,8 +70,8 @@ data class WireTransaction(val inputs: List, override val id: SecureHash get() = serialized.hash companion object { - fun deserialize(bits: SerializedBytes): WireTransaction { - val wtx = bits.bits.deserialize() + fun deserialize(bits: SerializedBytes, kryo: Kryo = THREAD_LOCAL_KRYO.get()): WireTransaction { + val wtx = bits.bits.deserialize(kryo) wtx.cachedBits = bits return wtx } diff --git a/src/main/kotlin/core/node/ClassLoader.kt b/core/src/main/kotlin/core/node/ClassLoader.kt similarity index 100% rename from src/main/kotlin/core/node/ClassLoader.kt rename to core/src/main/kotlin/core/node/ClassLoader.kt diff --git a/core/src/main/kotlin/core/node/services/Services.kt b/core/src/main/kotlin/core/node/services/Services.kt new file mode 100644 index 0000000000..5eefb4427d --- /dev/null +++ b/core/src/main/kotlin/core/node/services/Services.kt @@ -0,0 +1,33 @@ +package core.node.services + +import core.Attachment +import core.crypto.SecureHash +import java.io.InputStream + +/** + * An attachment store records potentially large binary objects, identified by their hash. Note that attachments are + * immutable and can never be erased once inserted! + */ +interface AttachmentStorage { + /** + * Returns a newly opened stream for the given locally stored attachment, or null if no such attachment is known. + * The returned stream must be closed when you are done with it to avoid resource leaks. You should probably wrap + * the result in a [JarInputStream] unless you're sending it somewhere, there is a convenience helper for this + * on [Attachment]. + */ + fun openAttachment(id: SecureHash): Attachment? + + /** + * 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. + * + * @throws FileAlreadyExistsException if the given byte stream has already been inserted. + * @throws IllegalArgumentException if the given byte stream is empty or a [JarInputStream] + * @throws IOException if something went wrong. + */ + fun importAttachment(jar: InputStream): SecureHash +} + diff --git a/core/src/main/kotlin/core/serialization/Kryo.kt b/core/src/main/kotlin/core/serialization/Kryo.kt index 5c05164a3a..33b650b50b 100644 --- a/core/src/main/kotlin/core/serialization/Kryo.kt +++ b/core/src/main/kotlin/core/serialization/Kryo.kt @@ -20,6 +20,7 @@ import core.* import core.crypto.SecureHash import core.crypto.generateKeyPair import core.crypto.sha256 +import core.node.services.AttachmentStorage import de.javakaffee.kryoserializers.ArraysAsListSerializer import org.objenesis.strategy.StdInstantiatorStrategy import java.io.ByteArrayOutputStream @@ -85,7 +86,7 @@ inline fun OpaqueBytes.deserialize(kryo: Kryo = THREAD_LOCAL_K } // The more specific deserialize version results in the bytes being cached, which is faster. @JvmName("SerializedBytesWireTransaction") -fun SerializedBytes.deserialize(): WireTransaction = WireTransaction.deserialize(this) +fun SerializedBytes.deserialize(kryo: Kryo = THREAD_LOCAL_KRYO.get()): WireTransaction = WireTransaction.deserialize(this, kryo) inline fun SerializedBytes.deserialize(kryo: Kryo = THREAD_LOCAL_KRYO.get(), includeClassName: Boolean = false): T = bits.deserialize(kryo, includeClassName) /** @@ -178,7 +179,7 @@ class ImmutableClassSerializer(val klass: KClass) : Serializer() } } -fun Kryo.useClassLoader(cl: ClassLoader, body: () -> Unit) { +inline fun Kryo.useClassLoader(cl: ClassLoader, body: () -> Unit) { val tmp = this.classLoader this.classLoader = cl try { @@ -191,7 +192,7 @@ fun Kryo.useClassLoader(cl: ClassLoader, body: () -> Unit) { } } -fun createKryo(k: Kryo = Kryo()): Kryo { +fun createKryo(k: Kryo = core.serialization.Kryo2()): Kryo { return k.apply { // Allow any class to be deserialized (this is insecure but for prototyping we don't care) isRegistrationRequired = false @@ -228,16 +229,22 @@ fun createKryo(k: Kryo = Kryo()): Kryo { var inputs = kryo.readClassAndObject( input ) as List var attachments = kryo.readClassAndObject( input ) as List - // had we access to AttachmentStorage here, a ClassLoader could be created + if (kryo is core.serialization.Kryo2) { - // val customClassLoader = createClassLoader( attachments ) - // kryo.useClassLoader(customClassLoader) { + val classLoader = core.node.ClassLoader.create( attachments?.map { kryo.attachmentStorage?.openAttachment(it)!! } ) - var outputs = kryo.readClassAndObject(input) as List - var commands = kryo.readClassAndObject(input) as List + kryo.useClassLoader(classLoader) { + var outputs = kryo.readClassAndObject(input) as List + var commands = kryo.readClassAndObject(input) as List + + return WireTransaction(inputs, attachments, outputs, commands) + } + } + + var outputs = kryo.readClassAndObject(input) as List + var commands = kryo.readClassAndObject(input) as List return WireTransaction(inputs, attachments, outputs, commands) - // } } }) @@ -262,3 +269,12 @@ fun createKryo(k: Kryo = Kryo()): Kryo { // TODO: See if we can make Lazy serialize properly so we can use "by lazy" in serialized object. } } + +/** + * Extends Kryo with a field for passing attachmentStorage to serializer for WireTransaction + * + * TODO: Think of better solution, or at least better name + */ +class Kryo2() : Kryo() { + var attachmentStorage: AttachmentStorage? = null +} diff --git a/src/main/kotlin/core/node/services/Services.kt b/src/main/kotlin/core/node/services/Services.kt index 538369c80a..3b8a654312 100644 --- a/src/main/kotlin/core/node/services/Services.kt +++ b/src/main/kotlin/core/node/services/Services.kt @@ -124,33 +124,6 @@ interface StorageService { val myLegalIdentityKey: KeyPair } -/** - * An attachment store records potentially large binary objects, identified by their hash. Note that attachments are - * immutable and can never be erased once inserted! - */ -interface AttachmentStorage { - /** - * Returns a newly opened stream for the given locally stored attachment, or null if no such attachment is known. - * The returned stream must be closed when you are done with it to avoid resource leaks. You should probably wrap - * the result in a [JarInputStream] unless you're sending it somewhere, there is a convenience helper for this - * on [Attachment]. - */ - fun openAttachment(id: SecureHash): Attachment? - - /** - * 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. - * - * @throws FileAlreadyExistsException if the given byte stream has already been inserted. - * @throws IllegalArgumentException if the given byte stream is empty or a [JarInputStream] - * @throws IOException if something went wrong. - */ - fun importAttachment(jar: InputStream): SecureHash -} - /** * Provides access to various metrics and ways to notify monitoring services of things, for sysadmin purposes. * This is not an interface because it is too lightweight to bother mocking out. diff --git a/src/test/kotlin/core/node/ClassLoaderTests.kt b/src/test/kotlin/core/node/ClassLoaderTests.kt index c09acc2e11..7ac2fbd877 100644 --- a/src/test/kotlin/core/node/ClassLoaderTests.kt +++ b/src/test/kotlin/core/node/ClassLoaderTests.kt @@ -1,11 +1,11 @@ package core.node -import core.Contract -import core.MockAttachmentStorage +import contracts.DUMMY_PROGRAM_ID +import contracts.DummyContract +import core.* import core.crypto.SecureHash -import core.serialization.createKryo -import core.serialization.deserialize -import core.serialization.serialize +import core.serialization.* +import core.testutils.MEGA_CORP import org.apache.commons.io.IOUtils import org.junit.Test import java.io.ByteArrayInputStream @@ -19,6 +19,12 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertNotNull +interface DummyContractBackdoor { + fun generateInitial(owner: PartyReference, magicNumber: Int) : TransactionBuilder + + fun inspectState(state: ContractState) : Int +} + class ClassLoaderTests { val ISOLATED_CONTRACTS_JAR_PATH = "contracts/isolated/build/libs/isolated.jar" @@ -174,8 +180,48 @@ class ClassLoaderTests { } @Test - fun `white list serialization`() { + fun `test serialization of WireTransaction with statically loaded contract`() { + val tx = DUMMY_PROGRAM_ID.generateInitial(MEGA_CORP.ref(0), 42) + val wireTransaction = tx.toWireTransaction() + + val bytes = wireTransaction.serialize() + + val copiedWireTransaction = bytes.deserialize() + + assertEquals(1, copiedWireTransaction.outputs.size) + + assertEquals(42, (copiedWireTransaction.outputs[0] as DummyContract.State).magicNumber) } + @Test + fun `test serialization of WireTransaction with dynamically loaded contract`() { + var child = URLClassLoader(arrayOf(URL("file", "", ISOLATED_CONTRACTS_JAR_PATH))) + + var contractClass = Class.forName("contracts.isolated.AnotherDummyContract", true, child) + var contract = contractClass.newInstance() as DummyContractBackdoor + + val tx = contract.generateInitial(MEGA_CORP.ref(0), 42) + + var storage = MockAttachmentStorage() + + // todo - think about better way to push attachmentStorage down to serializer + var kryo = THREAD_LOCAL_KRYO.get() as core.serialization.Kryo2 + kryo.attachmentStorage = storage + + var attachmentRef = storage.importAttachment( FileInputStream(ISOLATED_CONTRACTS_JAR_PATH) ) + + tx.addAttachment(storage.openAttachment(attachmentRef)!!) + + val wireTransaction = tx.toWireTransaction() + + val bytes = wireTransaction.serialize() + + val copiedWireTransaction = bytes.deserialize() + + assertEquals(1, copiedWireTransaction.outputs.size) + + var contract2 = copiedWireTransaction.outputs[0].contract as DummyContractBackdoor + assertEquals(42, contract2.inspectState( copiedWireTransaction.outputs[0] )) + } } \ No newline at end of file