[CORDA-1383]: Make SignedTransaction fully Jackson de/serialisable. (#3097)

This commit is contained in:
Michele Sollecito 2018-05-09 22:47:06 +07:00 committed by GitHub
parent fe88e9907c
commit c369680ccb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 148 additions and 27 deletions

View File

@ -2,17 +2,38 @@ package net.corda.client.jackson
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.core.*
import com.fasterxml.jackson.databind.*
import com.fasterxml.jackson.core.JsonFactory
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParseException
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonToken
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.Module
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.deser.std.NumberDeserializers
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.databind.ser.std.StdSerializer
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.KotlinModule
import net.corda.core.contracts.Amount
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateRef
import net.corda.core.crypto.*
import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.AddressFormatException
import net.corda.core.crypto.Base58
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.MerkleTree
import net.corda.core.crypto.PartialMerkleTree
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.SignatureMetadata
import net.corda.core.crypto.TransactionSignature
import net.corda.core.crypto.toStringShort
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.CordaX500Name
@ -24,12 +45,13 @@ import net.corda.core.node.services.IdentityService
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.transactions.*
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.utilities.OpaqueBytes
import net.corda.core.utilities.base58ToByteArray
import net.corda.core.utilities.base64ToByteArray
import net.corda.core.utilities.toBase64
import net.i2p.crypto.eddsa.EdDSAPublicKey
import java.math.BigDecimal
import java.security.PublicKey
import java.util.*
@ -99,12 +121,13 @@ object JacksonSupport {
addDeserializer(OpaqueBytes::class.java, OpaqueBytesDeserializer)
addSerializer(OpaqueBytes::class.java, OpaqueBytesSerializer)
listOf(TransactionSignatureSerde, SignedTransactionSerde).forEach { serde -> serde.applyTo(this) }
// For X.500 distinguished names
addDeserializer(CordaX500Name::class.java, CordaX500NameDeserializer)
addSerializer(CordaX500Name::class.java, CordaX500NameSerializer)
// Mixins for transaction types to prevent some properties from being serialized
setMixInAnnotation(SignedTransaction::class.java, SignedTransactionMixin::class.java)
setMixInAnnotation(WireTransaction::class.java, WireTransactionMixin::class.java)
}
}
@ -148,6 +171,90 @@ object JacksonSupport {
registerModule(KotlinModule())
}
private interface JsonSerde<TYPE> {
val type: Class<TYPE>
val serializer: JsonSerializer<TYPE>
val deserializer: JsonDeserializer<TYPE>
fun applyTo(module: SimpleModule) {
with(module) {
addSerializer(type, serializer)
addDeserializer(type, deserializer)
}
}
}
private inline fun <reified RESULT> JsonNode.get(fieldName: String, condition: (JsonNode) -> Boolean, mapper: ObjectMapper, parser: JsonParser): RESULT {
if (get(fieldName)?.let(condition) != true) {
JsonParseException(parser, "Missing required object field \"$fieldName\".")
}
return mapper.treeToValue(get(fieldName), RESULT::class.java)
}
private object TransactionSignatureSerde : JsonSerde<TransactionSignature> {
override val type: Class<TransactionSignature> = TransactionSignature::class.java
override val serializer = object : StdSerializer<TransactionSignature>(type) {
override fun serialize(value: TransactionSignature, json: JsonGenerator, serializers: SerializerProvider) {
with(json) {
writeStartObject()
writeObjectField("by", value.by)
writeObjectField("signatureMetadata", value.signatureMetadata)
writeObjectField("bytes", value.bytes)
writeObjectField("partialMerkleTree", value.partialMerkleTree)
writeEndObject()
}
}
}
override val deserializer = object : StdDeserializer<TransactionSignature>(type) {
override fun deserialize(parser: JsonParser, context: DeserializationContext): TransactionSignature {
val mapper = parser.codec as ObjectMapper
val json = mapper.readTree<JsonNode>(parser)
if (json.get("by")?.isTextual != true) {
JsonParseException(parser, "Missing required text field \"by\".")
}
val by = PublicKeyDeserializer.deserializeValue(json.get("by").textValue())
val signatureMetadata = json.get<SignatureMetadata>("signatureMetadata", JsonNode::isObject, mapper, parser)
val bytes = json.get<ByteArray>("bytes", JsonNode::isObject, mapper, parser)
val partialMerkleTree = json.get<PartialMerkleTree>("partialMerkleTree", JsonNode::isObject, mapper, parser)
return TransactionSignature(bytes, by, signatureMetadata, partialMerkleTree)
}
}
}
private object SignedTransactionSerde : JsonSerde<SignedTransaction> {
override val type: Class<SignedTransaction> = SignedTransaction::class.java
override val serializer = object : StdSerializer<SignedTransaction>(type) {
override fun serialize(value: SignedTransaction, json: JsonGenerator, serializers: SerializerProvider) {
with(json) {
writeStartObject()
writeObjectField("txBits", value.txBits.bytes)
writeObjectField("signatures", value.sigs)
writeEndObject()
}
}
}
override val deserializer = object : StdDeserializer<SignedTransaction>(type) {
override fun deserialize(parser: JsonParser, context: DeserializationContext): SignedTransaction {
val mapper = parser.codec as ObjectMapper
val json = mapper.readTree<JsonNode>(parser)
val txBits = json.get<ByteArray>("txBits", JsonNode::isTextual, mapper, parser)
val signatures = json.get<TransactionSignatures>("signatures", JsonNode::isArray, mapper, parser)
return SignedTransaction(SerializedBytes(txBits), signatures)
}
}
private class TransactionSignatures : ArrayList<TransactionSignature>()
}
object ToStringSerializer : JsonSerializer<Any>() {
override fun serialize(obj: Any, generator: JsonGenerator, provider: SerializerProvider) {
generator.writeString(obj.toString())
@ -282,11 +389,15 @@ object JacksonSupport {
object PublicKeyDeserializer : JsonDeserializer<PublicKey>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): PublicKey {
return deserializeValue(parser.text, parser)
}
internal fun deserializeValue(value: String, parser: JsonParser? = null): PublicKey {
return try {
val derBytes = parser.text.base64ToByteArray()
val derBytes = value.base64ToByteArray()
Crypto.decodePublicKey(derBytes)
} catch (e: Exception) {
throw JsonParseException(parser, "Invalid public key ${parser.text}: ${e.message}")
throw JsonParseException(parser, "Invalid public key $value: ${e.message}")
}
}
}

View File

@ -5,7 +5,11 @@ import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.contracts.Amount
import net.corda.core.cordapp.CordappProvider
import net.corda.core.crypto.*
import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.SignatureMetadata
import net.corda.core.crypto.TransactionSignature
import net.corda.core.identity.CordaX500Name
import net.corda.core.node.ServiceHub
import net.corda.core.transactions.SignedTransaction
@ -13,10 +17,12 @@ import net.corda.finance.USD
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.contracts.DummyContract
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.core.TestIdentity
import net.corda.testing.internal.rigorousMock
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@ -30,6 +36,7 @@ class JacksonSupportTest {
val SEED = BigInteger.valueOf(20170922L)!!
val mapper = JacksonSupport.createNonRpcMapper()
val ALICE_PUBKEY = TestIdentity(ALICE_NAME, 70).publicKey
val BOB_PUBKEY = TestIdentity(BOB_NAME, 70).publicKey
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
val MINI_CORP = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")).party
}
@ -99,26 +106,27 @@ class JacksonSupportTest {
}
@Test
fun writeTransaction() {
fun `wire transaction can be serialized and de-serialized`() {
val attachmentRef = SecureHash.randomSHA256()
doReturn(attachmentRef).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID)
doReturn(testNetworkParameters()).whenever(services).networkParameters
fun makeDummyTx(): SignedTransaction {
val wtx = DummyContract.generateInitial(1, DUMMY_NOTARY, MINI_CORP.ref(1))
.toWireTransaction(services)
val signatures = TransactionSignature(
ByteArray(1),
ALICE_PUBKEY,
SignatureMetadata(
1,
Crypto.findSignatureScheme(ALICE_PUBKEY).schemeNumberID
)
)
return SignedTransaction(wtx, listOf(signatures))
}
val writer = mapper.writer()
// We don't particularly care about the serialized format, just need to make sure it completes successfully.
writer.writeValueAsString(makeDummyTx())
val transaction = makeDummyTx()
val json = writer.writeValueAsString(transaction)
val deserializedTransaction = mapper.readValue(json, SignedTransaction::class.java)
assertThat(deserializedTransaction).isEqualTo(transaction)
}
private fun makeDummyTx(): SignedTransaction {
val wtx = DummyContract.generateInitial(1, DUMMY_NOTARY, MINI_CORP.ref(1))
.toWireTransaction(services)
val signatures = listOf(
TransactionSignature(ByteArray(1), ALICE_PUBKEY, SignatureMetadata(1, Crypto.findSignatureScheme(ALICE_PUBKEY).schemeNumberID)),
TransactionSignature(ByteArray(1), BOB_PUBKEY, SignatureMetadata(1, Crypto.findSignatureScheme(BOB_PUBKEY).schemeNumberID))
)
return SignedTransaction(wtx, signatures)
}
}

View File

@ -8,6 +8,8 @@ Unreleased
==========
* Fixed an error thrown by NodeVaultService upon recording a transaction with a number of inputs greater than the default page size.
* ``SignedTransaction`` can now be serialized to JSON and deserialized back into an object.
* Fixed incorrect computation of ``totalStates`` from ``otherResults`` in ``NodeVaultService``.
* Refactor AMQP Serializer to pass context object down the serialization call hierarchy. Will allow per thread