mirror of
https://github.com/corda/corda.git
synced 2025-01-29 15:43:55 +00:00
[CORDA-1383]: Make SignedTransaction fully Jackson de/serialisable. (#3097)
This commit is contained in:
parent
fe88e9907c
commit
c369680ccb
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user