mirror of
https://github.com/corda/corda.git
synced 2025-06-14 05:08:18 +00:00
CORDA-521: Backwards compatible Transactions using sub-Merkle trees (#1481)
* tx backwards compatibility + rebase * SHA256d definition
This commit is contained in:
committed by
josecoll
parent
477ea3a5e1
commit
6887947a4d
@ -7,20 +7,15 @@ import com.esotericsoftware.kryo.io.Output
|
||||
import com.esotericsoftware.kryo.serializers.CompatibleFieldSerializer
|
||||
import com.esotericsoftware.kryo.serializers.FieldSerializer
|
||||
import com.esotericsoftware.kryo.util.MapReferenceResolver
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.contracts.PrivacySalt
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.nodeapi.internal.AttachmentsClassLoader
|
||||
import net.corda.core.serialization.MissingAttachmentsException
|
||||
import net.corda.core.serialization.SerializeAsTokenContext
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.transactions.CoreTransaction
|
||||
import net.corda.core.transactions.NotaryChangeWireTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.transactions.*
|
||||
import net.i2p.crypto.eddsa.EdDSAPrivateKey
|
||||
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
||||
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec
|
||||
@ -241,42 +236,15 @@ fun Input.readBytesWithLength(): ByteArray {
|
||||
@ThreadSafe
|
||||
object WireTransactionSerializer : Serializer<WireTransaction>() {
|
||||
override fun write(kryo: Kryo, output: Output, obj: WireTransaction) {
|
||||
kryo.writeClassAndObject(output, obj.inputs)
|
||||
kryo.writeClassAndObject(output, obj.attachments)
|
||||
kryo.writeClassAndObject(output, obj.outputs)
|
||||
kryo.writeClassAndObject(output, obj.commands)
|
||||
kryo.writeClassAndObject(output, obj.notary)
|
||||
kryo.writeClassAndObject(output, obj.timeWindow)
|
||||
kryo.writeClassAndObject(output, obj.componentGroups)
|
||||
kryo.writeClassAndObject(output, obj.privacySalt)
|
||||
}
|
||||
|
||||
private fun attachmentsClassLoader(kryo: Kryo, attachmentHashes: List<SecureHash>): ClassLoader? {
|
||||
kryo.context[attachmentsClassLoaderEnabledPropertyName] as? Boolean ?: false || return null
|
||||
val serializationContext = kryo.serializationContext() ?: return null // Some tests don't set one.
|
||||
val missing = ArrayList<SecureHash>()
|
||||
val attachments = ArrayList<Attachment>()
|
||||
attachmentHashes.forEach { id ->
|
||||
serializationContext.serviceHub.attachments.openAttachment(id)?.let { attachments += it } ?: run { missing += id }
|
||||
}
|
||||
missing.isNotEmpty() && throw MissingAttachmentsException(missing)
|
||||
return AttachmentsClassLoader(attachments)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<WireTransaction>): WireTransaction {
|
||||
val inputs = kryo.readClassAndObject(input) as List<StateRef>
|
||||
val attachmentHashes = kryo.readClassAndObject(input) as List<SecureHash>
|
||||
|
||||
// If we're deserialising in the sandbox context, we use our special attachments classloader.
|
||||
// Otherwise we just assume the code we need is on the classpath already.
|
||||
kryo.useClassLoader(attachmentsClassLoader(kryo, attachmentHashes) ?: javaClass.classLoader) {
|
||||
val outputs = kryo.readClassAndObject(input) as List<TransactionState<ContractState>>
|
||||
val commands = kryo.readClassAndObject(input) as List<Command<*>>
|
||||
val notary = kryo.readClassAndObject(input) as Party?
|
||||
val timeWindow = kryo.readClassAndObject(input) as TimeWindow?
|
||||
val privacySalt = kryo.readClassAndObject(input) as PrivacySalt
|
||||
return WireTransaction(inputs, attachmentHashes, outputs, commands, notary, timeWindow, privacySalt)
|
||||
}
|
||||
val componentGroups = kryo.readClassAndObject(input) as List<ComponentGroup>
|
||||
val privacySalt = kryo.readClassAndObject(input) as PrivacySalt
|
||||
return WireTransaction(componentGroups, privacySalt)
|
||||
}
|
||||
}
|
||||
|
||||
@ -410,8 +378,7 @@ inline fun <reified T> readListOfLength(kryo: Kryo, input: Input, minLen: Int =
|
||||
if (elemCount < minLen) throw KryoException("Cannot deserialize list, too little elements. Minimum required: $minLen, got: $elemCount")
|
||||
if (expectedLen != null && elemCount != expectedLen)
|
||||
throw KryoException("Cannot deserialize list, expected length: $expectedLen, got: $elemCount.")
|
||||
val list = (1..elemCount).map { kryo.readClassAndObject(input) as T }
|
||||
return list
|
||||
return (1..elemCount).map { kryo.readClassAndObject(input) as T }
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -21,7 +21,6 @@ import net.corda.nodeapi.internal.serialization.withTokenContext
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.testing.MEGA_CORP
|
||||
import net.corda.testing.TestDependencyInjectionBase
|
||||
import net.corda.testing.kryoSpecific
|
||||
import net.corda.testing.node.MockAttachmentStorage
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.junit.Assert
|
||||
@ -75,7 +74,7 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() {
|
||||
}
|
||||
}
|
||||
|
||||
fun importJar(storage: AttachmentStorage) = ISOLATED_CONTRACTS_JAR_PATH.openStream().use { storage.importAttachment(it) }
|
||||
private fun importJar(storage: AttachmentStorage) = ISOLATED_CONTRACTS_JAR_PATH.openStream().use { storage.importAttachment(it) }
|
||||
|
||||
// These ClassLoaders work together to load 'AnotherDummyContract' in a disposable way, such that even though
|
||||
// the class may be on the unit test class path (due to default IDE settings, etc), it won't be loaded into the
|
||||
@ -83,10 +82,10 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() {
|
||||
// ensures we have precise control over where it's loaded.
|
||||
object FilteringClassLoader : ClassLoader() {
|
||||
override fun loadClass(name: String, resolve: Boolean): Class<*>? {
|
||||
if ("AnotherDummyContract" in name) {
|
||||
return null
|
||||
return if ("AnotherDummyContract" in name) {
|
||||
null
|
||||
} else
|
||||
return super.loadClass(name, resolve)
|
||||
super.loadClass(name, resolve)
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,7 +101,7 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() {
|
||||
assertEquals("helloworld", contract.declaredField<Any?>("magicString").value)
|
||||
}
|
||||
|
||||
fun fakeAttachment(filepath: String, content: String): ByteArray {
|
||||
private fun fakeAttachment(filepath: String, content: String): ByteArray {
|
||||
val bs = ByteArrayOutputStream()
|
||||
val js = JarOutputStream(bs)
|
||||
js.putNextEntry(ZipEntry(filepath))
|
||||
@ -112,7 +111,7 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() {
|
||||
return bs.toByteArray()
|
||||
}
|
||||
|
||||
fun readAttachment(attachment: Attachment, filepath: String): ByteArray {
|
||||
private fun readAttachment(attachment: Attachment, filepath: String): ByteArray {
|
||||
ByteArrayOutputStream().use {
|
||||
attachment.extractFile(filepath, it)
|
||||
return it.toByteArray()
|
||||
@ -193,7 +192,6 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() {
|
||||
assertEquals("helloworld", contract.declaredField<Any?>("magicString").value)
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `verify that contract DummyContract is in classPath`() {
|
||||
val contractClass = Class.forName("net.corda.nodeapi.AttachmentsClassLoaderTests\$AttachmentDummyContract")
|
||||
@ -202,7 +200,7 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() {
|
||||
assertNotNull(contract)
|
||||
}
|
||||
|
||||
fun createContract2Cash(): Contract {
|
||||
private fun createContract2Cash(): Contract {
|
||||
val cl = ClassLoaderForTests()
|
||||
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, cl)
|
||||
return contractClass.newInstance() as Contract
|
||||
@ -320,8 +318,8 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() {
|
||||
val bytes = run {
|
||||
val attachmentRef = importJar(storage)
|
||||
tx.addAttachment(storage.openAttachment(attachmentRef)!!.id)
|
||||
val wireTransaction = tx.toWireTransaction()
|
||||
wireTransaction.serialize(context = context)
|
||||
val wireTransaction = tx.toWireTransaction(serializationContext = context)
|
||||
wireTransaction.serialize()
|
||||
}
|
||||
val copiedWireTransaction = bytes.deserialize(context = context)
|
||||
assertEquals(1, copiedWireTransaction.outputs.size)
|
||||
@ -334,35 +332,36 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() {
|
||||
|
||||
@Test
|
||||
fun `test deserialize of WireTransaction where contract cannot be found`() {
|
||||
kryoSpecific<AttachmentsClassLoaderTests>("Kryo verifies/loads attachments on deserialization, whereas AMQP currently does not") {
|
||||
val child = ClassLoaderForTests()
|
||||
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, child)
|
||||
val contract = contractClass.newInstance() as DummyContractBackdoor
|
||||
val tx = contract.generateInitial(MEGA_CORP.ref(0), 42, DUMMY_NOTARY)
|
||||
val storage = MockAttachmentStorage()
|
||||
val child = ClassLoaderForTests()
|
||||
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, child)
|
||||
val contract = contractClass.newInstance() as DummyContractBackdoor
|
||||
val tx = contract.generateInitial(MEGA_CORP.ref(0), 42, DUMMY_NOTARY)
|
||||
val storage = MockAttachmentStorage()
|
||||
val context = SerializationFactory.defaultFactory.defaultContext.withWhitelisted(contract.javaClass)
|
||||
.withWhitelisted(Class.forName("net.corda.finance.contracts.isolated.AnotherDummyContract\$State", true, child))
|
||||
.withWhitelisted(Class.forName("net.corda.finance.contracts.isolated.AnotherDummyContract\$Commands\$Create", true, child))
|
||||
.withAttachmentStorage(storage)
|
||||
|
||||
// todo - think about better way to push attachmentStorage down to serializer
|
||||
val attachmentRef = importJar(storage)
|
||||
val bytes = run {
|
||||
// todo - think about better way to push attachmentStorage down to serializer
|
||||
val attachmentRef = importJar(storage)
|
||||
val bytes = run {
|
||||
|
||||
tx.addAttachment(storage.openAttachment(attachmentRef)!!.id)
|
||||
tx.addAttachment(storage.openAttachment(attachmentRef)!!.id)
|
||||
|
||||
val wireTransaction = tx.toWireTransaction()
|
||||
|
||||
wireTransaction.serialize(context = SerializationFactory.defaultFactory.defaultContext.withAttachmentStorage(storage))
|
||||
}
|
||||
// use empty attachmentStorage
|
||||
|
||||
val e = assertFailsWith(MissingAttachmentsException::class) {
|
||||
val mockAttStorage = MockAttachmentStorage()
|
||||
bytes.deserialize(context = SerializationFactory.defaultFactory.defaultContext.withAttachmentStorage(mockAttStorage))
|
||||
|
||||
if(mockAttStorage.openAttachment(attachmentRef) == null) {
|
||||
throw MissingAttachmentsException(listOf(attachmentRef))
|
||||
}
|
||||
}
|
||||
assertEquals(attachmentRef, e.ids.single())
|
||||
val wireTransaction = tx.toWireTransaction(serializationContext = context)
|
||||
wireTransaction.serialize()
|
||||
}
|
||||
// use empty attachmentStorage
|
||||
|
||||
val e = assertFailsWith(MissingAttachmentsException::class) {
|
||||
val mockAttStorage = MockAttachmentStorage()
|
||||
bytes.deserialize(context = SerializationFactory.defaultFactory.defaultContext.withAttachmentStorage(mockAttStorage))
|
||||
|
||||
if(mockAttStorage.openAttachment(attachmentRef) == null) {
|
||||
throw MissingAttachmentsException(listOf(attachmentRef))
|
||||
}
|
||||
}
|
||||
assertEquals(attachmentRef, e.ids.single())
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -395,10 +394,10 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() {
|
||||
val storage = MockAttachmentStorage()
|
||||
val attachmentRef = SecureHash.randomSHA256()
|
||||
val outboundContext = SerializationFactory.defaultFactory.defaultContext.withClassLoader(child)
|
||||
// Serialize with custom context to avoid populating the default context with the specially loaded class
|
||||
// Serialize with custom context to avoid populating the default context with the specially loaded class.
|
||||
val serialized = contract.serialize(context = outboundContext)
|
||||
|
||||
// Then deserialize with the attachment class loader associated with the attachment
|
||||
// Then deserialize with the attachment class loader associated with the attachment.
|
||||
val e = assertFailsWith(MissingAttachmentsException::class) {
|
||||
// We currently ignore annotations in attachments, so manually whitelist.
|
||||
val inboundContext = SerializationFactory
|
||||
|
Reference in New Issue
Block a user