WireTransaction deserialization using AttachmentStorage

This commit is contained in:
sofusmortensen 2016-03-24 12:06:41 +00:00
parent 1344bfd6bb
commit e5dbf5d2a8
9 changed files with 147 additions and 54 deletions

View File

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

View File

@ -0,0 +1,7 @@
package core.node
interface DummyContractBackdoor {
fun generateInitial(owner: core.PartyReference, magicNumber: Int) : core.TransactionBuilder
fun inspectState(state: core.ContractState) : Int
}

View File

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

View File

@ -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<StateRef>,
override val id: SecureHash get() = serialized.hash
companion object {
fun deserialize(bits: SerializedBytes<WireTransaction>): WireTransaction {
val wtx = bits.bits.deserialize<WireTransaction>()
fun deserialize(bits: SerializedBytes<WireTransaction>, kryo: Kryo = THREAD_LOCAL_KRYO.get()): WireTransaction {
val wtx = bits.bits.deserialize<WireTransaction>(kryo)
wtx.cachedBits = bits
return wtx
}

View File

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

View File

@ -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 <reified T : Any> 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<WireTransaction>.deserialize(): WireTransaction = WireTransaction.deserialize(this)
fun SerializedBytes<WireTransaction>.deserialize(kryo: Kryo = THREAD_LOCAL_KRYO.get()): WireTransaction = WireTransaction.deserialize(this, kryo)
inline fun <reified T : Any> SerializedBytes<T>.deserialize(kryo: Kryo = THREAD_LOCAL_KRYO.get(), includeClassName: Boolean = false): T = bits.deserialize(kryo, includeClassName)
/**
@ -178,7 +179,7 @@ class ImmutableClassSerializer<T : Any>(val klass: KClass<T>) : Serializer<T>()
}
}
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<StateRef>
var attachments = kryo.readClassAndObject( input ) as List<SecureHash>
// 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<ContractState>
var commands = kryo.readClassAndObject(input) as List<Command>
kryo.useClassLoader(classLoader) {
var outputs = kryo.readClassAndObject(input) as List<ContractState>
var commands = kryo.readClassAndObject(input) as List<Command>
return WireTransaction(inputs, attachments, outputs, commands)
}
}
var outputs = kryo.readClassAndObject(input) as List<ContractState>
var commands = kryo.readClassAndObject(input) as List<Command>
return WireTransaction(inputs, attachments, outputs, commands)
// }
}
})
@ -262,3 +269,12 @@ fun createKryo(k: Kryo = Kryo()): Kryo {
// TODO: See if we can make Lazy<T> 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
}

View File

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

View File

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