mirror of
https://github.com/corda/corda.git
synced 2025-04-07 19:34:41 +00:00
WireTransaction deserialization using AttachmentStorage
This commit is contained in:
parent
1344bfd6bb
commit
e5dbf5d2a8
@ -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
|
||||
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package core.node
|
||||
|
||||
interface DummyContractBackdoor {
|
||||
fun generateInitial(owner: core.PartyReference, magicNumber: Int) : core.TransactionBuilder
|
||||
|
||||
fun inspectState(state: core.ContractState) : Int
|
||||
}
|
@ -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) )
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
33
core/src/main/kotlin/core/node/services/Services.kt
Normal file
33
core/src/main/kotlin/core/node/services/Services.kt
Normal 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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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] ))
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user