mirror of
https://github.com/corda/corda.git
synced 2025-06-13 04:38:19 +00:00
[CORDA-941]: Add NetworkParameters contract implementation whitelist. (#2580)
This commit is contained in:
committed by
GitHub
parent
977836f4eb
commit
5be0e4b39e
@ -1,7 +1,9 @@
|
||||
package net.corda.nodeapi.internal
|
||||
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.contracts.ContractAttachment
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
@ -31,6 +33,10 @@ class AttachmentsClassLoader(attachments: List<Attachment>, parent: ClassLoader
|
||||
}
|
||||
|
||||
init {
|
||||
require(attachments.mapNotNull { it as? ContractAttachment }.none { it.uploader != DEPLOYED_CORDAPP_UPLOADER }) {
|
||||
"Attempting to load Contract Attachments downloaded from the network"
|
||||
}
|
||||
|
||||
for (attachment in attachments) {
|
||||
attachment.openAsJAR().use { jar ->
|
||||
while (true) {
|
||||
|
@ -0,0 +1,41 @@
|
||||
package net.corda.nodeapi.internal
|
||||
|
||||
import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner
|
||||
import net.corda.core.contracts.Contract
|
||||
import net.corda.core.contracts.ContractClassName
|
||||
import net.corda.core.internal.copyTo
|
||||
import net.corda.core.internal.deleteIfExists
|
||||
import net.corda.core.internal.read
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.lang.reflect.Modifier
|
||||
import java.net.URLClassLoader
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardCopyOption
|
||||
|
||||
/**
|
||||
* Scans the jar for contracts.
|
||||
* @returns: found contract class names or null if none found
|
||||
*/
|
||||
fun scanJarForContracts(cordappJarPath: String): List<ContractClassName> {
|
||||
val currentClassLoader = Contract::class.java.classLoader
|
||||
val scanResult = FastClasspathScanner().addClassLoader(currentClassLoader).overrideClasspath(cordappJarPath, Paths.get(Contract::class.java.protectionDomain.codeSource.location.toURI()).toString()).scan()
|
||||
val contracts = (scanResult.getNamesOfClassesImplementing(Contract::class.qualifiedName) ).distinct()
|
||||
|
||||
// Only keep instantiable contracts
|
||||
val classLoader = URLClassLoader(arrayOf(File(cordappJarPath).toURL()), currentClassLoader)
|
||||
val concreteContracts = contracts.map(classLoader::loadClass).filter { !it.isInterface && !Modifier.isAbstract(it.modifiers) }
|
||||
return concreteContracts.map { it.name }
|
||||
}
|
||||
|
||||
fun <T> withContractsInJar(jarInputStream: InputStream, withContracts: (List<ContractClassName>, InputStream) -> T): T {
|
||||
val tempFile = Files.createTempFile("attachment", ".jar")
|
||||
try {
|
||||
jarInputStream.copyTo(tempFile, StandardCopyOption.REPLACE_EXISTING)
|
||||
val contracts = scanJarForContracts(tempFile.toAbsolutePath().toString())
|
||||
return tempFile.read { withContracts(contracts, it) }
|
||||
} finally {
|
||||
tempFile.deleteIfExists()
|
||||
}
|
||||
}
|
@ -1,25 +1,33 @@
|
||||
package net.corda.nodeapi.internal.network
|
||||
|
||||
import com.google.common.hash.Hashing
|
||||
import com.google.common.hash.HashingInputStream
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import net.corda.cordform.CordformNode
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.SecureHash.Companion.parse
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.internal.concurrent.fork
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.NotaryInfo
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.internal.SerializationEnvironmentImpl
|
||||
import net.corda.core.serialization.internal._contextSerializationEnv
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||
import net.corda.nodeapi.internal.scanJarForContracts
|
||||
import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT
|
||||
import net.corda.nodeapi.internal.serialization.CordaSerializationMagic
|
||||
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
|
||||
import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme
|
||||
import net.corda.nodeapi.internal.serialization.kryo.AbstractKryoSerializationScheme
|
||||
import net.corda.nodeapi.internal.serialization.kryo.kryoMagic
|
||||
import java.io.File
|
||||
import java.io.PrintStream
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
@ -43,15 +51,17 @@ class NetworkBootstrapper {
|
||||
)
|
||||
|
||||
private const val LOGS_DIR_NAME = "logs"
|
||||
private const val WHITELIST_FILE_NAME = "whitelist.txt"
|
||||
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
val arg = args.singleOrNull() ?: throw IllegalArgumentException("Expecting single argument which is the nodes' parent directory")
|
||||
NetworkBootstrapper().bootstrap(Paths.get(arg).toAbsolutePath().normalize())
|
||||
val baseNodeDirectory = args.firstOrNull() ?: throw IllegalArgumentException("Expecting first argument which is the nodes' parent directory")
|
||||
val cordapps = if (args.size > 1) args.toList().drop(1) else null
|
||||
NetworkBootstrapper().bootstrap(Paths.get(baseNodeDirectory).toAbsolutePath().normalize(), cordapps)
|
||||
}
|
||||
}
|
||||
|
||||
fun bootstrap(directory: Path) {
|
||||
fun bootstrap(directory: Path, cordapps: List<String>?) {
|
||||
directory.createDirectories()
|
||||
println("Bootstrapping local network in $directory")
|
||||
generateDirectoriesIfNeeded(directory)
|
||||
@ -68,7 +78,10 @@ class NetworkBootstrapper {
|
||||
println("Gathering notary identities")
|
||||
val notaryInfos = gatherNotaryInfos(nodeInfoFiles)
|
||||
println("Notary identities to be used in network-parameters file: ${notaryInfos.joinToString("; ") { it.prettyPrint() }}")
|
||||
installNetworkParameters(notaryInfos, nodeDirs)
|
||||
val mergedWhiteList = generateWhitelist(directory / WHITELIST_FILE_NAME, cordapps?.distinct())
|
||||
println("Updating whitelist.")
|
||||
overwriteWhitelist(directory / WHITELIST_FILE_NAME, mergedWhiteList)
|
||||
installNetworkParameters(notaryInfos, nodeDirs, mergedWhiteList)
|
||||
println("Bootstrapping complete!")
|
||||
} finally {
|
||||
_contextSerializationEnv.set(null)
|
||||
@ -84,8 +97,7 @@ class NetworkBootstrapper {
|
||||
for (confFile in confFiles) {
|
||||
val nodeName = confFile.fileName.toString().removeSuffix(".conf")
|
||||
println("Generating directory for $nodeName")
|
||||
val nodeDir = (directory / nodeName)
|
||||
if (!nodeDir.exists()) { nodeDir.createDirectory() }
|
||||
val nodeDir = (directory / nodeName).createDirectories()
|
||||
confFile.moveTo(nodeDir / "node.conf", StandardCopyOption.REPLACE_EXISTING)
|
||||
Files.copy(cordaJar, (nodeDir / "corda.jar"), StandardCopyOption.REPLACE_EXISTING)
|
||||
}
|
||||
@ -157,7 +169,7 @@ class NetworkBootstrapper {
|
||||
}.distinct() // We need distinct as nodes part of a distributed notary share the same notary identity
|
||||
}
|
||||
|
||||
private fun installNetworkParameters(notaryInfos: List<NotaryInfo>, nodeDirs: List<Path>) {
|
||||
private fun installNetworkParameters(notaryInfos: List<NotaryInfo>, nodeDirs: List<Path>, whitelist: Map<String, List<AttachmentId>>) {
|
||||
// TODO Add config for minimumPlatformVersion, maxMessageSize and maxTransactionSize
|
||||
val copier = NetworkParametersCopier(NetworkParameters(
|
||||
minimumPlatformVersion = 1,
|
||||
@ -165,12 +177,58 @@ class NetworkBootstrapper {
|
||||
modifiedTime = Instant.now(),
|
||||
maxMessageSize = 10485760,
|
||||
maxTransactionSize = Int.MAX_VALUE,
|
||||
epoch = 1
|
||||
epoch = 1,
|
||||
whitelistedContractImplementations = whitelist
|
||||
), overwriteFile = true)
|
||||
|
||||
nodeDirs.forEach { copier.install(it) }
|
||||
}
|
||||
|
||||
private fun generateWhitelist(whitelistFile: Path, cordapps: List<String>?): Map<String, List<AttachmentId>> {
|
||||
val existingWhitelist = if (whitelistFile.exists()) readContractWhitelist(whitelistFile) else emptyMap()
|
||||
|
||||
println("Found existing whitelist: $existingWhitelist")
|
||||
|
||||
val newWhiteList = cordapps?.flatMap { cordappJarPath ->
|
||||
val jarHash = getJarHash(cordappJarPath)
|
||||
scanJarForContracts(cordappJarPath).map { contract ->
|
||||
contract to jarHash
|
||||
}
|
||||
}?.toMap() ?: emptyMap()
|
||||
|
||||
println("Calculating whitelist for current cordapps: $newWhiteList")
|
||||
|
||||
val merged = (newWhiteList.keys + existingWhitelist.keys).map { contractClassName ->
|
||||
val existing = existingWhitelist[contractClassName] ?: emptyList()
|
||||
val newHash = newWhiteList[contractClassName]
|
||||
contractClassName to (if (newHash == null || newHash in existing) existing else existing + newHash)
|
||||
}.toMap()
|
||||
|
||||
println("Final whitelist: $merged")
|
||||
|
||||
return merged
|
||||
}
|
||||
|
||||
private fun overwriteWhitelist(whitelistFile: Path, mergedWhiteList: Map<String, List<AttachmentId>>) {
|
||||
PrintStream(whitelistFile.toFile().outputStream()).use { out ->
|
||||
mergedWhiteList.forEach { (contract, attachments )->
|
||||
out.println("${contract}:${attachments.joinToString(",")}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getJarHash(cordappPath: String): AttachmentId = File(cordappPath).inputStream().use { jar ->
|
||||
val hs = HashingInputStream(Hashing.sha256(), jar)
|
||||
hs.readBytes()
|
||||
SecureHash.SHA256(hs.hash().asBytes())
|
||||
}
|
||||
|
||||
private fun readContractWhitelist(file: Path): Map<String, List<AttachmentId>> = file.toFile().readLines()
|
||||
.map { line -> line.split(":") }
|
||||
.map { (contract, attachmentIds) ->
|
||||
contract to (attachmentIds.split(",").map(::parse))
|
||||
}.toMap()
|
||||
|
||||
private fun NotaryInfo.prettyPrint(): String = "${identity.name} (${if (validating) "" else "non-"}validating)"
|
||||
|
||||
private fun NodeInfo.notaryIdentity(): Party {
|
||||
|
@ -21,12 +21,12 @@ class ContractAttachmentSerializer(factory: SerializerFactory) : CustomSerialize
|
||||
} catch (e: Exception) {
|
||||
throw MissingAttachmentsException(listOf(obj.id))
|
||||
}
|
||||
return ContractAttachmentProxy(GeneratedAttachment(bytes), obj.contract)
|
||||
return ContractAttachmentProxy(GeneratedAttachment(bytes), obj.contract, obj.additionalContracts, obj.uploader)
|
||||
}
|
||||
|
||||
override fun fromProxy(proxy: ContractAttachmentProxy): ContractAttachment {
|
||||
return ContractAttachment(proxy.attachment, proxy.contract)
|
||||
return ContractAttachment(proxy.attachment, proxy.contract, proxy.contracts, proxy.uploader)
|
||||
}
|
||||
|
||||
data class ContractAttachmentProxy(val attachment: Attachment, val contract: ContractClassName)
|
||||
data class ContractAttachmentProxy(val attachment: Attachment, val contract: ContractClassName, val contracts: Set<ContractClassName>, val uploader: String?)
|
||||
}
|
@ -12,6 +12,7 @@ import de.javakaffee.kryoserializers.BitSetSerializer
|
||||
import de.javakaffee.kryoserializers.UnmodifiableCollectionsSerializer
|
||||
import de.javakaffee.kryoserializers.guava.*
|
||||
import net.corda.core.contracts.ContractAttachment
|
||||
import net.corda.core.contracts.ContractClassName
|
||||
import net.corda.core.contracts.PrivacySalt
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.SecureHash
|
||||
@ -206,29 +207,34 @@ object DefaultKryoCustomizer {
|
||||
output.writeBytesWithLength(buffer.toByteArray())
|
||||
}
|
||||
output.writeString(obj.contract)
|
||||
kryo.writeClassAndObject(output, obj.additionalContracts)
|
||||
output.writeString(obj.uploader)
|
||||
}
|
||||
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<ContractAttachment>): ContractAttachment {
|
||||
if (kryo.serializationContext() != null) {
|
||||
val attachmentHash = SecureHash.SHA256(input.readBytes(32))
|
||||
val contract = input.readString()
|
||||
|
||||
val additionalContracts = kryo.readClassAndObject(input) as Set<ContractClassName>
|
||||
val uploader = input.readString()
|
||||
val context = kryo.serializationContext()!!
|
||||
val attachmentStorage = context.serviceHub.attachments
|
||||
|
||||
val lazyAttachment = object : AbstractAttachment({
|
||||
val attachment = attachmentStorage.openAttachment(attachmentHash) ?: throw MissingAttachmentsException(listOf(attachmentHash))
|
||||
val attachment = attachmentStorage.openAttachment(attachmentHash)
|
||||
?: throw MissingAttachmentsException(listOf(attachmentHash))
|
||||
attachment.open().readBytes()
|
||||
}) {
|
||||
override val id = attachmentHash
|
||||
}
|
||||
|
||||
return ContractAttachment(lazyAttachment, contract)
|
||||
return ContractAttachment(lazyAttachment, contract, additionalContracts, uploader)
|
||||
} else {
|
||||
val attachment = GeneratedAttachment(input.readBytesWithLength())
|
||||
val contract = input.readString()
|
||||
|
||||
return ContractAttachment(attachment, contract)
|
||||
val additionalContracts = kryo.readClassAndObject(input) as Set<ContractClassName>
|
||||
val uploader = input.readString()
|
||||
return ContractAttachment(attachment, contract, additionalContracts, uploader)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.node.internal.cordapp.CordappLoader
|
||||
import net.corda.node.internal.cordapp.CordappProviderImpl
|
||||
import net.corda.testing.common.internal.testNetworkParameters
|
||||
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
import net.corda.testing.core.TestIdentity
|
||||
@ -59,7 +60,8 @@ class AttachmentsClassLoaderStaticContractTests {
|
||||
}
|
||||
|
||||
private val serviceHub = rigorousMock<ServicesForResolution>().also {
|
||||
doReturn(CordappProviderImpl(CordappLoader.createWithTestPackages(listOf("net.corda.nodeapi.internal")), MockCordappConfigProvider(), MockAttachmentStorage())).whenever(it).cordappProvider
|
||||
doReturn(CordappProviderImpl(CordappLoader.createWithTestPackages(listOf("net.corda.nodeapi.internal")), MockCordappConfigProvider(), MockAttachmentStorage(), testNetworkParameters().whitelistedContractImplementations)).whenever(it).cordappProvider
|
||||
doReturn(testNetworkParameters()).whenever(it).networkParameters
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -18,6 +18,7 @@ import net.corda.nodeapi.DummyContractBackdoor
|
||||
import net.corda.nodeapi.internal.serialization.SerializeAsTokenContextImpl
|
||||
import net.corda.nodeapi.internal.serialization.attachmentsClassLoaderEnabledPropertyName
|
||||
import net.corda.nodeapi.internal.serialization.withTokenContext
|
||||
import net.corda.testing.common.internal.testNetworkParameters
|
||||
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
import net.corda.testing.core.TestIdentity
|
||||
@ -58,12 +59,15 @@ class AttachmentsClassLoaderTests {
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule()
|
||||
private val attachments = MockAttachmentStorage()
|
||||
private val cordappProvider = CordappProviderImpl(CordappLoader.createDevMode(listOf(ISOLATED_CONTRACTS_JAR_PATH)), MockCordappConfigProvider(), attachments)
|
||||
private val networkParameters = testNetworkParameters()
|
||||
private val cordappProvider = CordappProviderImpl(CordappLoader.createDevMode(listOf(ISOLATED_CONTRACTS_JAR_PATH)), MockCordappConfigProvider(), attachments, networkParameters.whitelistedContractImplementations)
|
||||
private val cordapp get() = cordappProvider.cordapps.first()
|
||||
private val attachmentId get() = cordappProvider.getCordappAttachmentId(cordapp)!!
|
||||
private val appContext get() = cordappProvider.getAppContext(cordapp)
|
||||
private val serviceHub = rigorousMock<ServiceHub>().also {
|
||||
doReturn(attachments).whenever(it).attachments
|
||||
doReturn(cordappProvider).whenever(it).cordappProvider
|
||||
doReturn(networkParameters).whenever(it).networkParameters
|
||||
}
|
||||
|
||||
// These ClassLoaders work together to load 'AnotherDummyContract' in a disposable way, such that even though
|
||||
@ -279,7 +283,7 @@ class AttachmentsClassLoaderTests {
|
||||
.withClassLoader(child)
|
||||
|
||||
val bytes = run {
|
||||
val wireTransaction = tx.toWireTransaction(cordappProvider, context)
|
||||
val wireTransaction = tx.toWireTransaction(serviceHub, context)
|
||||
wireTransaction.serialize(context = context)
|
||||
}
|
||||
val copiedWireTransaction = bytes.deserialize(context = context)
|
||||
@ -303,7 +307,7 @@ class AttachmentsClassLoaderTests {
|
||||
val outboundContext = SerializationFactory.defaultFactory.defaultContext
|
||||
.withServiceHub(serviceHub)
|
||||
.withClassLoader(child)
|
||||
val wireTransaction = tx.toWireTransaction(cordappProvider, outboundContext)
|
||||
val wireTransaction = tx.toWireTransaction(serviceHub, outboundContext)
|
||||
wireTransaction.serialize(context = outboundContext)
|
||||
}
|
||||
// use empty attachmentStorage
|
||||
|
@ -43,6 +43,7 @@ class ContractAttachmentSerializerTest {
|
||||
|
||||
assertEquals(contractAttachment.id, deserialized.attachment.id)
|
||||
assertEquals(contractAttachment.contract, deserialized.contract)
|
||||
assertEquals(contractAttachment.additionalContracts, deserialized.additionalContracts)
|
||||
assertArrayEquals(contractAttachment.open().readBytes(), deserialized.open().readBytes())
|
||||
}
|
||||
|
||||
@ -58,6 +59,7 @@ class ContractAttachmentSerializerTest {
|
||||
|
||||
assertEquals(contractAttachment.id, deserialized.attachment.id)
|
||||
assertEquals(contractAttachment.contract, deserialized.contract)
|
||||
assertEquals(contractAttachment.additionalContracts, deserialized.additionalContracts)
|
||||
assertArrayEquals(contractAttachment.open().readBytes(), deserialized.open().readBytes())
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ import com.esotericsoftware.kryo.io.Output
|
||||
import com.esotericsoftware.kryo.util.DefaultClassResolver
|
||||
import com.esotericsoftware.kryo.util.MapReferenceResolver
|
||||
import com.nhaarman.mockito_kotlin.*
|
||||
import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER
|
||||
import net.corda.core.node.services.AttachmentStorage
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.nodeapi.internal.AttachmentsClassLoader
|
||||
@ -195,7 +196,7 @@ class CordaClassResolverTests {
|
||||
CordaClassResolver(emptyWhitelistContext).getRegistration(DefaultSerializable::class.java)
|
||||
}
|
||||
|
||||
private fun importJar(storage: AttachmentStorage) = AttachmentsClassLoaderTests.ISOLATED_CONTRACTS_JAR_PATH.openStream().use { storage.importAttachment(it) }
|
||||
private fun importJar(storage: AttachmentStorage, uploader: String = DEPLOYED_CORDAPP_UPLOADER) = AttachmentsClassLoaderTests.ISOLATED_CONTRACTS_JAR_PATH.openStream().use { storage.importAttachment(it, uploader, "") }
|
||||
|
||||
@Test(expected = KryoException::class)
|
||||
fun `Annotation does not work in conjunction with AttachmentClassLoader annotation`() {
|
||||
@ -206,6 +207,15 @@ class CordaClassResolverTests {
|
||||
CordaClassResolver(emptyWhitelistContext).getRegistration(attachedClass)
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun `Attempt to load contract attachment with the incorrect uploader should fails with IAE`() {
|
||||
val storage = MockAttachmentStorage()
|
||||
val attachmentHash = importJar(storage, "some_uploader")
|
||||
val classLoader = AttachmentsClassLoader(arrayOf(attachmentHash).map { storage.openAttachment(it)!! })
|
||||
val attachedClass = Class.forName("net.corda.finance.contracts.isolated.AnotherDummyContract", true, classLoader)
|
||||
CordaClassResolver(emptyWhitelistContext).getRegistration(attachedClass)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Annotation is inherited from interfaces`() {
|
||||
CordaClassResolver(emptyWhitelistContext).getRegistration(SerializableViaInterface::class.java)
|
||||
|
@ -523,7 +523,7 @@ class EvolvabilityTests {
|
||||
val resource = "networkParams.<corda version>.<commit sha>"
|
||||
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
|
||||
val networkParameters = NetworkParameters(
|
||||
3, listOf(NotaryInfo(DUMMY_NOTARY, false)),1000, 1000, Instant.EPOCH, 1 )
|
||||
3, listOf(NotaryInfo(DUMMY_NOTARY, false)),1000, 1000, Instant.EPOCH, 1, emptyMap())
|
||||
|
||||
val sf = testDefaultFactory()
|
||||
sf.register(net.corda.nodeapi.internal.serialization.amqp.custom.InstantSerializer(sf))
|
||||
|
@ -1071,6 +1071,7 @@ class SerializationOutputTests(private val compression: CordaSerializationEncodi
|
||||
val obj2 = serdes(obj, factory, factory2, expectedEqual = false, expectDeserializedEqual = false)
|
||||
assertEquals(obj.id, obj2.attachment.id)
|
||||
assertEquals(obj.contract, obj2.contract)
|
||||
assertEquals(obj.additionalContracts, obj2.additionalContracts)
|
||||
assertArrayEquals(obj.open().readBytes(), obj2.open().readBytes())
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user