CORDA-521: Backwards compatible Transactions using sub-Merkle trees (#1481)

* tx backwards compatibility + rebase

* SHA256d definition
This commit is contained in:
Konstantinos Chalkias
2017-09-20 13:11:58 +01:00
committed by josecoll
parent 477ea3a5e1
commit 6887947a4d
20 changed files with 804 additions and 367 deletions

View File

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

View File

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