From ac179aa9ab7979faed0c355a014c0414de94c551 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Wed, 11 Jul 2018 14:50:51 +0100 Subject: [PATCH] CORDA-1664: Blob inspector able to display SignedTransaction blobs dumped from a node's db. (#3559) --- tools/blobinspector/build.gradle | 7 +- .../{Main.kt => BlobInspector.kt} | 43 ++++++----- .../corda/blobinspector/BlobInspectorTest.kt | 67 ++++++++++++++++++ .../net/corda/blobinspector/cash-stx-db.blob | Bin 0 -> 11244 bytes .../net/corda/blobinspector/cash-wtx.blob | Bin 0 -> 14875 bytes .../corda/blobinspector/network-parameters | Bin 0 -> 4559 bytes .../net/corda/blobinspector/node-info | Bin 0 -> 4718 bytes 7 files changed, 98 insertions(+), 19 deletions(-) rename tools/blobinspector/src/main/kotlin/net/corda/blobinspector/{Main.kt => BlobInspector.kt} (83%) create mode 100644 tools/blobinspector/src/test/kotlin/net/corda/blobinspector/BlobInspectorTest.kt create mode 100644 tools/blobinspector/src/test/resources/net/corda/blobinspector/cash-stx-db.blob create mode 100644 tools/blobinspector/src/test/resources/net/corda/blobinspector/cash-wtx.blob create mode 100644 tools/blobinspector/src/test/resources/net/corda/blobinspector/network-parameters create mode 100644 tools/blobinspector/src/test/resources/net/corda/blobinspector/node-info diff --git a/tools/blobinspector/build.gradle b/tools/blobinspector/build.gradle index 26a15d84dc..4cd7ef4386 100644 --- a/tools/blobinspector/build.gradle +++ b/tools/blobinspector/build.gradle @@ -10,8 +10,11 @@ dependencies { compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" compile "com.jcabi:jcabi-manifests:$jcabi_manifests_version" + testCompile(project(':test-utils')) { + exclude module: 'node-api' + exclude module: 'finance' + } testCompile project(':test-utils') - testCompile "junit:junit:$junit_version" } jar { @@ -24,7 +27,7 @@ jar { manifest { attributes( 'Automatic-Module-Name': 'net.corda.blobinspector', - 'Main-Class': 'net.corda.blobinspector.MainKt' + 'Main-Class': 'net.corda.blobinspector.BlobInspectorKt' ) } } diff --git a/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/Main.kt b/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt similarity index 83% rename from tools/blobinspector/src/main/kotlin/net/corda/blobinspector/Main.kt rename to tools/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt index 9590f39d8e..389688f766 100644 --- a/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/Main.kt +++ b/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt @@ -7,12 +7,13 @@ import net.corda.client.jackson.JacksonSupport import net.corda.core.internal.isRegularFile import net.corda.core.internal.rootMessage import net.corda.core.serialization.SerializationContext -import net.corda.core.serialization.SerializationFactory +import net.corda.core.serialization.SerializationDefaults import net.corda.core.serialization.deserialize import net.corda.core.serialization.internal.SerializationEnvironmentImpl import net.corda.core.serialization.internal._contextSerializationEnv import net.corda.core.utilities.sequence import net.corda.serialization.internal.AMQP_P2P_CONTEXT +import net.corda.serialization.internal.AMQP_STORAGE_CONTEXT import net.corda.serialization.internal.CordaSerializationMagic import net.corda.serialization.internal.SerializationFactoryImpl import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme @@ -20,13 +21,14 @@ import net.corda.serialization.internal.amqp.DeserializationInput import net.corda.serialization.internal.amqp.amqpMagic import picocli.CommandLine import picocli.CommandLine.* +import java.io.PrintStream import java.net.MalformedURLException import java.net.URL import java.nio.file.Paths import kotlin.system.exitProcess fun main(args: Array) { - val main = Main() + val main = BlobInspector() try { CommandLine.run(main, *args) } catch (e: ExecutionException) { @@ -47,9 +49,9 @@ fun main(args: Array) { showDefaultValues = true, description = ["Inspect AMQP serialised binary blobs"] ) -class Main : Runnable { +class BlobInspector : Runnable { @Parameters(index = "0", paramLabel = "SOURCE", description = ["URL or file path to the blob"], converter = [SourceConverter::class]) - private var source: URL? = null + var source: URL? = null @Option(names = ["--format"], paramLabel = "type", description = ["Output format. Possible values: [YAML, JSON]"]) private var formatType: FormatType = FormatType.YAML @@ -64,7 +66,9 @@ class Main : Runnable { @Option(names = ["--verbose"], description = ["Enable verbose output"]) var verbose: Boolean = false - override fun run() { + override fun run() = run(System.out) + + fun run(out: PrintStream) { if (verbose) { System.setProperty("logLevel", "trace") } @@ -78,39 +82,44 @@ class Main : Runnable { if (schema) { val envelope = DeserializationInput.getEnvelope(bytes) - println(envelope.schema) - println() - println(envelope.transformsSchema) - println() + out.println(envelope.schema) + out.println() + out.println(envelope.transformsSchema) + out.println() } - initialiseSerialization() - val factory = when (formatType) { FormatType.YAML -> YAMLFactory() FormatType.JSON -> JsonFactory() } + val mapper = JacksonSupport.createNonRpcMapper(factory, fullParties) - // Deserialise with the lenient carpenter as we only care for the AMQP field getters - val deserialized = bytes.deserialize(context = SerializationFactory.defaultFactory.defaultContext.withLenientCarpenter()) - println(deserialized.javaClass.name) - mapper.writeValue(System.out, deserialized) + initialiseSerialization() + try { + val deserialized = bytes.deserialize(context = SerializationDefaults.STORAGE_CONTEXT) + out.println(deserialized.javaClass.name) + mapper.writeValue(out, deserialized) + } finally { + _contextSerializationEnv.set(null) + } } private fun initialiseSerialization() { + // Deserialise with the lenient carpenter as we only care for the AMQP field getters _contextSerializationEnv.set(SerializationEnvironmentImpl( SerializationFactoryImpl().apply { registerScheme(AMQPInspectorSerializationScheme) }, - AMQP_P2P_CONTEXT + p2pContext = AMQP_P2P_CONTEXT.withLenientCarpenter(), + storageContext = AMQP_STORAGE_CONTEXT.withLenientCarpenter() )) } } private object AMQPInspectorSerializationScheme : AbstractAMQPSerializationScheme(emptyList()) { override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean { - return magic == amqpMagic && target == SerializationContext.UseCase.P2P + return magic == amqpMagic } override fun rpcClientSerializerFactory(context: SerializationContext) = throw UnsupportedOperationException() override fun rpcServerSerializerFactory(context: SerializationContext) = throw UnsupportedOperationException() diff --git a/tools/blobinspector/src/test/kotlin/net/corda/blobinspector/BlobInspectorTest.kt b/tools/blobinspector/src/test/kotlin/net/corda/blobinspector/BlobInspectorTest.kt new file mode 100644 index 0000000000..65e1223c4f --- /dev/null +++ b/tools/blobinspector/src/test/kotlin/net/corda/blobinspector/BlobInspectorTest.kt @@ -0,0 +1,67 @@ +package net.corda.blobinspector + +import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.SignedDataWithCert +import net.corda.core.node.NetworkParameters +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.WireTransaction +import net.corda.testing.common.internal.checkNotOnClasspath +import org.apache.commons.io.output.WriterOutputStream +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import java.io.PrintStream +import java.io.StringWriter +import java.nio.charset.StandardCharsets.UTF_8 + +class BlobInspectorTest { + private val blobInspector = BlobInspector() + + @Test + fun `network-parameters file`() { + val output = run("network-parameters") + assertThat(output) + .startsWith(SignedDataWithCert::class.java.name) + .contains(NetworkParameters::class.java.name) + .contains(CordaX500Name("Notary Service", "Zurich", "CH").toString()) // Name of the notary in the network parameters + } + + @Test + fun `node-info file`() { + checkNotOnClassPath("net.corda.nodeapi.internal.SignedNodeInfo") + val output = run("node-info") + assertThat(output) + .startsWith("net.corda.nodeapi.internal.SignedNodeInfo") + .contains(CordaX500Name("BankOfCorda", "New York", "US").toString()) + } + + @Test + fun `WireTransaction with Cash state`() { + checkNotOnClassPath("net.corda.finance.contracts.asset.Cash\$State") + val output = run("cash-wtx.blob") + assertThat(output) + .startsWith(WireTransaction::class.java.name) + .contains("net.corda.finance.contracts.asset.Cash\$State") + } + + @Test + fun `SignedTransaction with Cash state taken from node db`() { + checkNotOnClassPath("net.corda.finance.contracts.asset.Cash\$State") + val output = run("cash-stx-db.blob") + assertThat(output) + .startsWith(SignedTransaction::class.java.name) + .contains("net.corda.finance.contracts.asset.Cash\$State") + } + + private fun run(resourceName: String): String { + blobInspector.source = javaClass.getResource(resourceName) + val writer = StringWriter() + blobInspector.run(PrintStream(WriterOutputStream(writer, UTF_8))) + val output = writer.toString() + println(output) + return output + } + + private fun checkNotOnClassPath(className: String) { + checkNotOnClasspath(className) { "The Blob Inspector does not have this as a dependency." } + } +} diff --git a/tools/blobinspector/src/test/resources/net/corda/blobinspector/cash-stx-db.blob b/tools/blobinspector/src/test/resources/net/corda/blobinspector/cash-stx-db.blob new file mode 100644 index 0000000000000000000000000000000000000000..21f93d8367c021c98c805da630920af9b0556b14 GIT binary patch literal 11244 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 literal 0 HcmV?d00001 diff --git a/tools/blobinspector/src/test/resources/net/corda/blobinspector/network-parameters b/tools/blobinspector/src/test/resources/net/corda/blobinspector/network-parameters new file mode 100644 index 0000000000000000000000000000000000000000..d6494a03d9db6f312053a0265541a9d7b5f19c33 GIT binary patch literal 4559 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 literal 0 HcmV?d00001