mirror of
https://github.com/corda/corda.git
synced 2025-01-20 19:49:25 +00:00
CORDA-1383: Cleaned up the JSON format of WireTransaction and SignedTransaction (#3248)
In particular correctly outputs the custom state and command objects in the txs. Also fixed up deserialisation back into the transaction objects.
This commit is contained in:
parent
ed70fea3a7
commit
7b09795795
2
.idea/compiler.xml
generated
2
.idea/compiler.xml
generated
@ -71,6 +71,8 @@
|
||||
<module name="example-code_integrationTest" target="1.8" />
|
||||
<module name="example-code_main" target="1.8" />
|
||||
<module name="example-code_test" target="1.8" />
|
||||
<module name="experimental-blobinspector_main" target="1.8" />
|
||||
<module name="experimental-blobinspector_test" target="1.8" />
|
||||
<module name="experimental-kryo-hook_main" target="1.8" />
|
||||
<module name="experimental-kryo-hook_test" target="1.8" />
|
||||
<module name="experimental_main" target="1.8" />
|
||||
|
@ -37,7 +37,7 @@ buildscript {
|
||||
* https://issues.apache.org/jira/browse/ARTEMIS-1559
|
||||
*/
|
||||
ext.artemis_version = '2.5.0'
|
||||
ext.jackson_version = '2.9.3'
|
||||
ext.jackson_version = '2.9.5'
|
||||
ext.jetty_version = '9.4.7.v20170914'
|
||||
ext.jersey_version = '2.25'
|
||||
ext.assertj_version = '3.8.0'
|
||||
|
@ -6,6 +6,7 @@ import com.fasterxml.jackson.core.*
|
||||
import com.fasterxml.jackson.databind.*
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier
|
||||
import com.fasterxml.jackson.databind.deser.std.NumberDeserializers
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
||||
@ -22,9 +23,7 @@ import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.identity.*
|
||||
import net.corda.core.internal.CertRole
|
||||
import net.corda.core.internal.VisibleForTesting
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.services.IdentityService
|
||||
@ -37,7 +36,6 @@ import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.parsePublicKeyBase58
|
||||
import net.corda.core.utilities.toBase58String
|
||||
import org.bouncycastle.asn1.x509.KeyPurposeId
|
||||
import java.lang.reflect.Modifier
|
||||
import java.math.BigDecimal
|
||||
import java.nio.charset.StandardCharsets.UTF_8
|
||||
import java.security.PublicKey
|
||||
@ -169,7 +167,9 @@ object JacksonSupport {
|
||||
addSerializer(Date::class.java, DateSerializer)
|
||||
})
|
||||
registerModule(CordaModule())
|
||||
registerModule(KotlinModule())
|
||||
registerModule(KotlinModule().apply {
|
||||
setDeserializerModifier(KotlinObjectDeserializerModifier)
|
||||
})
|
||||
|
||||
addMixIn(BigDecimal::class.java, BigDecimalMixin::class.java)
|
||||
addMixIn(X500Principal::class.java, X500PrincipalMixin::class.java)
|
||||
@ -178,6 +178,19 @@ object JacksonSupport {
|
||||
}
|
||||
}
|
||||
|
||||
private object KotlinObjectDeserializerModifier : BeanDeserializerModifier() {
|
||||
override fun modifyDeserializer(config: DeserializationConfig,
|
||||
beanDesc: BeanDescription,
|
||||
deserializer: JsonDeserializer<*>): JsonDeserializer<*> {
|
||||
val objectInstance = beanDesc.beanClass.kotlinObjectInstance
|
||||
return if (objectInstance != null) KotlinObjectDeserializer(objectInstance) else deserializer
|
||||
}
|
||||
}
|
||||
|
||||
private class KotlinObjectDeserializer<T>(private val objectInstance: T) : JsonDeserializer<T>() {
|
||||
override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): T = objectInstance
|
||||
}
|
||||
|
||||
@ToStringSerialize
|
||||
@JsonDeserialize(using = NumberDeserializers.BigDecimalDeserializer::class)
|
||||
private interface BigDecimalMixin
|
||||
@ -210,7 +223,7 @@ object JacksonSupport {
|
||||
|
||||
val keyPurposeIds = KeyPurposeId::class.java
|
||||
.fields
|
||||
.filter { Modifier.isStatic(it.modifiers) && it.type == KeyPurposeId::class.java }
|
||||
.filter { it.isStatic && it.type == KeyPurposeId::class.java }
|
||||
.associateBy({ (it.get(null) as KeyPurposeId).id }, { it.name })
|
||||
|
||||
val knownExtensions = setOf(
|
||||
@ -235,7 +248,7 @@ object JacksonSupport {
|
||||
writeObjectField("issuerUniqueID", value.issuerUniqueID)
|
||||
writeObjectField("subjectUniqueID", value.subjectUniqueID)
|
||||
writeObjectField("keyUsage", value.keyUsage?.asList()?.mapIndexedNotNull { i, flag -> if (flag) keyUsages[i] else null })
|
||||
writeObjectField("extendedKeyUsage", value.extendedKeyUsage.map { keyPurposeIds.getOrDefault(it, it) })
|
||||
writeObjectField("extendedKeyUsage", value.extendedKeyUsage.map { keyPurposeIds[it] ?: it })
|
||||
jsonObject("basicConstraints") {
|
||||
val isCa = value.basicConstraints != -1
|
||||
writeBooleanField("isCA", isCa)
|
||||
|
@ -2,30 +2,39 @@
|
||||
|
||||
package net.corda.client.jackson.internal
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo
|
||||
import com.fasterxml.jackson.annotation.JsonValue
|
||||
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.*
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||
import com.fasterxml.jackson.databind.node.IntNode
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode
|
||||
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter
|
||||
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier
|
||||
import com.google.common.primitives.Booleans
|
||||
import net.corda.client.jackson.JacksonSupport
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.crypto.PartialMerkleTree.PartialTree
|
||||
import net.corda.core.identity.*
|
||||
import net.corda.core.internal.DigitalSignatureWithCert
|
||||
import net.corda.core.internal.kotlinObjectInstance
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
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.utilities.ByteSequence
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.parseAsHex
|
||||
import net.corda.core.utilities.toHexString
|
||||
import net.corda.serialization.internal.AllWhitelist
|
||||
import net.corda.serialization.internal.amqp.SerializerFactory
|
||||
import net.corda.serialization.internal.amqp.constructorForDeserialization
|
||||
@ -33,6 +42,7 @@ import net.corda.serialization.internal.amqp.hasCordaSerializable
|
||||
import net.corda.serialization.internal.amqp.propertiesForSerialization
|
||||
import java.security.PublicKey
|
||||
import java.security.cert.CertPath
|
||||
import java.time.Instant
|
||||
|
||||
class CordaModule : SimpleModule("corda-core") {
|
||||
override fun setupModule(context: SetupContext) {
|
||||
@ -50,12 +60,20 @@ class CordaModule : SimpleModule("corda-core") {
|
||||
context.setMixInAnnotations(PublicKey::class.java, PublicKeyMixin::class.java)
|
||||
context.setMixInAnnotations(ByteSequence::class.java, ByteSequenceMixin::class.java)
|
||||
context.setMixInAnnotations(SecureHash.SHA256::class.java, SecureHashSHA256Mixin::class.java)
|
||||
context.setMixInAnnotations(SecureHash::class.java, SecureHashSHA256Mixin::class.java)
|
||||
context.setMixInAnnotations(SerializedBytes::class.java, SerializedBytesMixin::class.java)
|
||||
context.setMixInAnnotations(DigitalSignature.WithKey::class.java, ByteSequenceWithPropertiesMixin::class.java)
|
||||
context.setMixInAnnotations(DigitalSignatureWithCert::class.java, ByteSequenceWithPropertiesMixin::class.java)
|
||||
context.setMixInAnnotations(TransactionSignature::class.java, ByteSequenceWithPropertiesMixin::class.java)
|
||||
context.setMixInAnnotations(SignedTransaction::class.java, SignedTransactionMixin::class.java)
|
||||
context.setMixInAnnotations(WireTransaction::class.java, JacksonSupport.WireTransactionMixin::class.java)
|
||||
context.setMixInAnnotations(WireTransaction::class.java, WireTransactionMixin::class.java)
|
||||
context.setMixInAnnotations(TransactionState::class.java, TransactionStateMixin::class.java)
|
||||
context.setMixInAnnotations(Command::class.java, CommandMixin::class.java)
|
||||
context.setMixInAnnotations(TimeWindow::class.java, TimeWindowMixin::class.java)
|
||||
context.setMixInAnnotations(PrivacySalt::class.java, PrivacySaltMixin::class.java)
|
||||
context.setMixInAnnotations(SignatureScheme::class.java, SignatureSchemeMixin::class.java)
|
||||
context.setMixInAnnotations(SignatureMetadata::class.java, SignatureMetadataMixin::class.java)
|
||||
context.setMixInAnnotations(PartialTree::class.java, PartialTreeMixin::class.java)
|
||||
context.setMixInAnnotations(NodeInfo::class.java, NodeInfoMixin::class.java)
|
||||
}
|
||||
}
|
||||
@ -64,21 +82,23 @@ class CordaModule : SimpleModule("corda-core") {
|
||||
* Use the same properties that AMQP serialization uses if the POJO is @CordaSerializable
|
||||
*/
|
||||
private class CordaSerializableBeanSerializerModifier : BeanSerializerModifier() {
|
||||
// We need a SerializerFactory when scanning for properties but don't actually use it so any will do
|
||||
private val serializerFactory = SerializerFactory(AllWhitelist, Thread.currentThread().contextClassLoader)
|
||||
// We need to pass in a SerializerFactory when scanning for properties, but don't actually do any serialisation so any will do.
|
||||
private val serializerFactory = SerializerFactory(AllWhitelist, javaClass.classLoader)
|
||||
|
||||
override fun changeProperties(config: SerializationConfig,
|
||||
beanDesc: BeanDescription,
|
||||
beanProperties: MutableList<BeanPropertyWriter>): MutableList<BeanPropertyWriter> {
|
||||
if (hasCordaSerializable(beanDesc.beanClass)) {
|
||||
val ctor = constructorForDeserialization(beanDesc.beanClass)
|
||||
val amqpProperties = propertiesForSerialization(ctor, beanDesc.beanClass, serializerFactory)
|
||||
val beanClass = beanDesc.beanClass
|
||||
if (hasCordaSerializable(beanClass) && beanClass.kotlinObjectInstance == null) {
|
||||
val ctor = constructorForDeserialization(beanClass)
|
||||
val amqpProperties = propertiesForSerialization(ctor, beanClass, serializerFactory)
|
||||
.serializationOrder
|
||||
.map { it.serializer.name }
|
||||
beanProperties.removeIf { it.name !in amqpProperties }
|
||||
(amqpProperties - beanProperties.map { it.name }).let {
|
||||
val propertyRenames = beanDesc.findProperties().associateBy({ it.name }, { it.internalName })
|
||||
(amqpProperties - propertyRenames.values).let {
|
||||
check(it.isEmpty()) { "Jackson didn't provide serialisers for $it" }
|
||||
}
|
||||
beanProperties.removeIf { propertyRenames[it.name] !in amqpProperties }
|
||||
}
|
||||
return beanProperties
|
||||
}
|
||||
@ -88,11 +108,7 @@ private class CordaSerializableBeanSerializerModifier : BeanSerializerModifier()
|
||||
@JsonDeserialize(using = NetworkHostAndPortDeserializer::class)
|
||||
private interface NetworkHostAndPortMixin
|
||||
|
||||
private class NetworkHostAndPortDeserializer : JsonDeserializer<NetworkHostAndPort>() {
|
||||
override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): NetworkHostAndPort {
|
||||
return NetworkHostAndPort.parse(parser.text)
|
||||
}
|
||||
}
|
||||
private class NetworkHostAndPortDeserializer : SimpleDeserializer<NetworkHostAndPort>({ NetworkHostAndPort.parse(text) })
|
||||
|
||||
@JsonSerialize(using = PartyAndCertificateSerializer::class)
|
||||
// TODO Add deserialization which follows the same lookup logic as Party
|
||||
@ -102,14 +118,14 @@ private class PartyAndCertificateSerializer : JsonSerializer<PartyAndCertificate
|
||||
override fun serialize(value: PartyAndCertificate, gen: JsonGenerator, serializers: SerializerProvider) {
|
||||
val mapper = gen.codec as JacksonSupport.PartyObjectMapper
|
||||
if (mapper.isFullParties) {
|
||||
gen.writeObject(PartyAndCertificateWrapper(value.name, value.certPath))
|
||||
gen.writeObject(PartyAndCertificateJson(value.name, value.certPath))
|
||||
} else {
|
||||
gen.writeObject(value.party)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class PartyAndCertificateWrapper(val name: CordaX500Name, val certPath: CertPath)
|
||||
private class PartyAndCertificateJson(val name: CordaX500Name, val certPath: CertPath)
|
||||
|
||||
@JsonSerialize(using = SignedTransactionSerializer::class)
|
||||
@JsonDeserialize(using = SignedTransactionDeserializer::class)
|
||||
@ -117,18 +133,226 @@ private interface SignedTransactionMixin
|
||||
|
||||
private class SignedTransactionSerializer : JsonSerializer<SignedTransaction>() {
|
||||
override fun serialize(value: SignedTransaction, gen: JsonGenerator, serializers: SerializerProvider) {
|
||||
gen.writeObject(SignedTransactionWrapper(value.txBits.bytes, value.sigs))
|
||||
val core = value.coreTransaction
|
||||
val stxJson = when (core) {
|
||||
is WireTransaction -> StxJson(wire = core, signatures = value.sigs)
|
||||
is FilteredTransaction -> StxJson(filtered = core, signatures = value.sigs)
|
||||
is NotaryChangeWireTransaction -> StxJson(notaryChangeWire = core, signatures = value.sigs)
|
||||
is ContractUpgradeWireTransaction -> StxJson(contractUpgradeWire = core, signatures = value.sigs)
|
||||
is ContractUpgradeFilteredTransaction -> StxJson(contractUpgradeFiltered = core, signatures = value.sigs)
|
||||
else -> throw IllegalArgumentException("Don't know about ${core.javaClass}")
|
||||
}
|
||||
gen.writeObject(stxJson)
|
||||
}
|
||||
}
|
||||
|
||||
private class SignedTransactionDeserializer : JsonDeserializer<SignedTransaction>() {
|
||||
override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): SignedTransaction {
|
||||
val wrapper = parser.readValueAs<SignedTransactionWrapper>()
|
||||
return SignedTransaction(SerializedBytes(wrapper.txBits), wrapper.signatures)
|
||||
val wrapper = parser.readValueAs<StxJson>()
|
||||
val core = wrapper.run { wire ?: filtered ?: notaryChangeWire ?: contractUpgradeWire ?: contractUpgradeFiltered!! }
|
||||
return SignedTransaction(core, wrapper.signatures)
|
||||
}
|
||||
}
|
||||
|
||||
private class SignedTransactionWrapper(val txBits: ByteArray, val signatures: List<TransactionSignature>)
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
private data class StxJson(
|
||||
val wire: WireTransaction? = null,
|
||||
val filtered: FilteredTransaction? = null,
|
||||
val notaryChangeWire: NotaryChangeWireTransaction? = null,
|
||||
val contractUpgradeWire: ContractUpgradeWireTransaction? = null,
|
||||
val contractUpgradeFiltered: ContractUpgradeFilteredTransaction? = null,
|
||||
val signatures: List<TransactionSignature>
|
||||
) {
|
||||
init {
|
||||
val count = Booleans.countTrue(wire != null, filtered != null, notaryChangeWire != null, contractUpgradeWire != null, contractUpgradeFiltered != null)
|
||||
require(count == 1) { this }
|
||||
}
|
||||
}
|
||||
|
||||
@JsonSerialize(using = WireTransactionSerializer::class)
|
||||
@JsonDeserialize(using = WireTransactionDeserializer::class)
|
||||
private interface WireTransactionMixin
|
||||
|
||||
private class WireTransactionSerializer : JsonSerializer<WireTransaction>() {
|
||||
override fun serialize(value: WireTransaction, gen: JsonGenerator, serializers: SerializerProvider) {
|
||||
gen.writeObject(WireTransactionJson(
|
||||
value.id,
|
||||
value.notary,
|
||||
value.inputs,
|
||||
value.outputs,
|
||||
value.commands,
|
||||
value.timeWindow,
|
||||
value.attachments,
|
||||
value.privacySalt
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
private class WireTransactionDeserializer : JsonDeserializer<WireTransaction>() {
|
||||
override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): WireTransaction {
|
||||
val wrapper = parser.readValueAs<WireTransactionJson>()
|
||||
val componentGroups = WireTransaction.createComponentGroups(
|
||||
wrapper.inputs,
|
||||
wrapper.outputs,
|
||||
wrapper.commands,
|
||||
wrapper.attachments,
|
||||
wrapper.notary,
|
||||
wrapper.timeWindow
|
||||
)
|
||||
return WireTransaction(componentGroups, wrapper.privacySalt)
|
||||
}
|
||||
}
|
||||
|
||||
private class WireTransactionJson(val id: SecureHash,
|
||||
val notary: Party?,
|
||||
val inputs: List<StateRef>,
|
||||
val outputs: List<TransactionState<*>>,
|
||||
val commands: List<Command<*>>,
|
||||
val timeWindow: TimeWindow?,
|
||||
val attachments: List<SecureHash>,
|
||||
val privacySalt: PrivacySalt)
|
||||
|
||||
private interface TransactionStateMixin {
|
||||
@get:JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
|
||||
val data: ContractState
|
||||
@get:JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
|
||||
val constraint: AttachmentConstraint
|
||||
}
|
||||
|
||||
private interface CommandMixin {
|
||||
@get:JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
|
||||
val value: CommandData
|
||||
}
|
||||
|
||||
@JsonDeserialize(using = TimeWindowDeserializer::class)
|
||||
private interface TimeWindowMixin
|
||||
|
||||
private class TimeWindowDeserializer : JsonDeserializer<TimeWindow>() {
|
||||
override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): TimeWindow {
|
||||
return parser.readValueAs<TimeWindowJson>().run {
|
||||
when {
|
||||
fromTime != null && untilTime != null -> TimeWindow.between(fromTime, untilTime)
|
||||
fromTime != null -> TimeWindow.fromOnly(fromTime)
|
||||
untilTime != null -> TimeWindow.untilOnly(untilTime)
|
||||
else -> throw JsonParseException(parser, "Neither fromTime nor untilTime exists for TimeWindow")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class TimeWindowJson(val fromTime: Instant?, val untilTime: Instant?)
|
||||
|
||||
@JsonSerialize(using = PrivacySaltSerializer::class)
|
||||
@JsonDeserialize(using = PrivacySaltDeserializer::class)
|
||||
private interface PrivacySaltMixin
|
||||
|
||||
private class PrivacySaltSerializer : JsonSerializer<PrivacySalt>() {
|
||||
override fun serialize(value: PrivacySalt, gen: JsonGenerator, serializers: SerializerProvider) {
|
||||
gen.writeString(value.bytes.toHexString())
|
||||
}
|
||||
}
|
||||
|
||||
private class PrivacySaltDeserializer : SimpleDeserializer<PrivacySalt>({ PrivacySalt(text.parseAsHex()) })
|
||||
|
||||
// TODO Add a lookup function by number ID in Crypto
|
||||
private val signatureSchemesByNumberID = Crypto.supportedSignatureSchemes().associateBy { it.schemeNumberID }
|
||||
|
||||
@JsonSerialize(using = SignatureMetadataSerializer::class)
|
||||
@JsonDeserialize(using = SignatureMetadataDeserializer::class)
|
||||
private interface SignatureMetadataMixin
|
||||
|
||||
private class SignatureMetadataSerializer : JsonSerializer<SignatureMetadata>() {
|
||||
override fun serialize(value: SignatureMetadata, gen: JsonGenerator, serializers: SerializerProvider) {
|
||||
gen.jsonObject {
|
||||
writeNumberField("platformVersion", value.platformVersion)
|
||||
writeObjectField("scheme", value.schemeNumberID.let { signatureSchemesByNumberID[it] ?: it })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class SignatureMetadataDeserializer : JsonDeserializer<SignatureMetadata>() {
|
||||
override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): SignatureMetadata {
|
||||
val json = parser.readValueAsTree<ObjectNode>()
|
||||
val scheme = json["scheme"]
|
||||
val schemeNumberID = if (scheme is IntNode) {
|
||||
scheme.intValue()
|
||||
} else {
|
||||
Crypto.findSignatureScheme(scheme.textValue()).schemeNumberID
|
||||
}
|
||||
return SignatureMetadata(json["platformVersion"].intValue(), schemeNumberID)
|
||||
}
|
||||
}
|
||||
|
||||
@JsonSerialize(using = PartialTreeSerializer::class)
|
||||
@JsonDeserialize(using = PartialTreeDeserializer::class)
|
||||
private interface PartialTreeMixin
|
||||
|
||||
private class PartialTreeSerializer : JsonSerializer<PartialTree>() {
|
||||
override fun serialize(value: PartialTree, gen: JsonGenerator, serializers: SerializerProvider) {
|
||||
gen.writeObject(convert(value))
|
||||
}
|
||||
|
||||
private fun convert(tree: PartialTree): PartialTreeJson {
|
||||
return when (tree) {
|
||||
is PartialTree.IncludedLeaf -> PartialTreeJson(includedLeaf = tree.hash)
|
||||
is PartialTree.Leaf -> PartialTreeJson(leaf = tree.hash)
|
||||
is PartialTree.Node -> PartialTreeJson(left = convert(tree.left), right = convert(tree.right))
|
||||
else -> throw IllegalArgumentException("Don't know how to serialize $tree")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class PartialTreeDeserializer : JsonDeserializer<PartialTree>() {
|
||||
override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): PartialTree {
|
||||
return convert(parser.readValueAs(PartialTreeJson::class.java))
|
||||
}
|
||||
|
||||
private fun convert(wrapper: PartialTreeJson): PartialTree {
|
||||
return wrapper.run {
|
||||
when {
|
||||
includedLeaf != null -> PartialTree.IncludedLeaf(includedLeaf)
|
||||
leaf != null -> PartialTree.Leaf(leaf)
|
||||
else -> PartialTree.Node(convert(left!!), convert(right!!))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
private class PartialTreeJson(val includedLeaf: SecureHash? = null,
|
||||
val leaf: SecureHash? = null,
|
||||
val left: PartialTreeJson? = null,
|
||||
val right: PartialTreeJson? = null) {
|
||||
init {
|
||||
if (includedLeaf != null) {
|
||||
require(leaf == null && left == null && right == null)
|
||||
} else if (leaf != null) {
|
||||
require(left == null && right == null)
|
||||
} else {
|
||||
require(left != null && right != null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JsonSerialize(using = SignatureSchemeSerializer::class)
|
||||
@JsonDeserialize(using = SignatureSchemeDeserializer::class)
|
||||
private interface SignatureSchemeMixin
|
||||
|
||||
private class SignatureSchemeSerializer : JsonSerializer<SignatureScheme>() {
|
||||
override fun serialize(value: SignatureScheme, gen: JsonGenerator, serializers: SerializerProvider) {
|
||||
gen.writeString(value.schemeCodeName)
|
||||
}
|
||||
}
|
||||
|
||||
private class SignatureSchemeDeserializer : JsonDeserializer<SignatureScheme>() {
|
||||
override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): SignatureScheme {
|
||||
return if (parser.currentToken == JsonToken.VALUE_NUMBER_INT) {
|
||||
signatureSchemesByNumberID[parser.intValue] ?: throw JsonParseException(parser, "Unable to find SignatureScheme ${parser.text}")
|
||||
} else {
|
||||
Crypto.findSignatureScheme(parser.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JsonSerialize(using = SerializedBytesSerializer::class)
|
||||
@JsonDeserialize(using = SerializedBytesDeserializer::class)
|
||||
|
@ -3,6 +3,8 @@ package net.corda.client.jackson.internal
|
||||
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.databind.DeserializationContext
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||
@ -20,6 +22,14 @@ inline fun <reified T> JsonParser.readValueAs(): T = readValueAs(T::class.java)
|
||||
|
||||
inline fun <reified T : Any> JsonNode.valueAs(mapper: ObjectMapper): T = mapper.convertValue(this)
|
||||
|
||||
inline fun <reified T : Any> JsonNode.childrenAs(mapper: ObjectMapper): List<T> {
|
||||
return elements().asSequence().map { it.valueAs<T>(mapper) }.toList()
|
||||
}
|
||||
|
||||
@JacksonAnnotationsInside
|
||||
@JsonSerialize(using = ToStringSerializer::class)
|
||||
annotation class ToStringSerialize
|
||||
annotation class ToStringSerialize
|
||||
|
||||
abstract class SimpleDeserializer<T>(private val func: JsonParser.() -> T) : JsonDeserializer<T>() {
|
||||
override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): T = func(parser)
|
||||
}
|
||||
|
@ -3,17 +3,20 @@ package net.corda.client.jackson
|
||||
import com.fasterxml.jackson.core.JsonFactory
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.databind.node.BinaryNode
|
||||
import com.fasterxml.jackson.databind.node.IntNode
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode
|
||||
import com.fasterxml.jackson.databind.node.TextNode
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
|
||||
import com.fasterxml.jackson.module.kotlin.convertValue
|
||||
import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.client.jackson.internal.childrenAs
|
||||
import net.corda.client.jackson.internal.valueAs
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.cordapp.CordappProvider
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.PartialMerkleTree.PartialTree
|
||||
import net.corda.core.identity.*
|
||||
import net.corda.core.internal.DigitalSignatureWithCert
|
||||
import net.corda.core.node.NodeInfo
|
||||
@ -21,7 +24,10 @@ import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.CoreTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.utilities.*
|
||||
import net.corda.finance.USD
|
||||
import net.corda.nodeapi.internal.crypto.x509Certificates
|
||||
@ -39,10 +45,11 @@ import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import org.junit.runners.Parameterized.Parameters
|
||||
import java.math.BigInteger
|
||||
import java.nio.charset.StandardCharsets.*
|
||||
import java.nio.charset.StandardCharsets.UTF_8
|
||||
import java.security.PublicKey
|
||||
import java.security.cert.CertPath
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import javax.security.auth.x500.X500Principal
|
||||
import kotlin.collections.ArrayList
|
||||
@ -52,7 +59,7 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
|
||||
private companion object {
|
||||
val SEED: BigInteger = BigInteger.valueOf(20170922L)
|
||||
val ALICE_PUBKEY = TestIdentity(ALICE_NAME, 70).publicKey
|
||||
val BOB_PUBKEY = TestIdentity(BOB_NAME, 70).publicKey
|
||||
val BOB_PUBKEY = TestIdentity(BOB_NAME, 80).publicKey
|
||||
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
|
||||
val MINI_CORP = TestIdentity(CordaX500Name("MiniCorp", "London", "GB"))
|
||||
|
||||
@ -76,6 +83,7 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
|
||||
services = rigorousMock()
|
||||
cordappProvider = rigorousMock()
|
||||
doReturn(cordappProvider).whenever(services).cordappProvider
|
||||
doReturn(testNetworkParameters()).whenever(services).networkParameters
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -182,43 +190,190 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
|
||||
|
||||
@Test
|
||||
fun TransactionSignature() {
|
||||
val metadata = SignatureMetadata(1, 1)
|
||||
val transactionSignature = TransactionSignature(secureRandomBytes(128), BOB_PUBKEY, metadata)
|
||||
val signatureMetadata = SignatureMetadata(1, 1)
|
||||
val partialMerkleTree = PartialMerkleTree(PartialTree.Node(
|
||||
left = PartialTree.Leaf(SecureHash.randomSHA256()),
|
||||
right = PartialTree.IncludedLeaf(SecureHash.randomSHA256())
|
||||
))
|
||||
val transactionSignature = TransactionSignature(secureRandomBytes(128), BOB_PUBKEY, signatureMetadata, partialMerkleTree)
|
||||
val json = mapper.valueToTree<ObjectNode>(transactionSignature)
|
||||
val (bytes, by, signatureMetadata, partialMerkleTree) = json.assertHasOnlyFields(
|
||||
val (bytesJson, byJson, signatureMetadataJson, partialMerkleTreeJson) = json.assertHasOnlyFields(
|
||||
"bytes",
|
||||
"by",
|
||||
"signatureMetadata",
|
||||
"partialMerkleTree"
|
||||
)
|
||||
assertThat(bytes.binaryValue()).isEqualTo(transactionSignature.bytes)
|
||||
assertThat(by.valueAs<PublicKey>(mapper)).isEqualTo(BOB_PUBKEY)
|
||||
assertThat(signatureMetadata.valueAs<SignatureMetadata>(mapper)).isEqualTo(metadata)
|
||||
assertThat(partialMerkleTree.isNull).isTrue()
|
||||
assertThat(bytesJson.binaryValue()).isEqualTo(transactionSignature.bytes)
|
||||
assertThat(byJson.valueAs<PublicKey>(mapper)).isEqualTo(BOB_PUBKEY)
|
||||
assertThat(signatureMetadataJson.valueAs<SignatureMetadata>(mapper)).isEqualTo(signatureMetadata)
|
||||
assertThat(partialMerkleTreeJson.valueAs<PartialMerkleTree>(mapper).root).isEqualTo(partialMerkleTree.root)
|
||||
assertThat(mapper.convertValue<TransactionSignature>(json)).isEqualTo(transactionSignature)
|
||||
}
|
||||
|
||||
// TODO Add test for PartialMerkleTree
|
||||
|
||||
@Test
|
||||
fun SignedTransaction() {
|
||||
val attachmentRef = SecureHash.randomSHA256()
|
||||
doReturn(attachmentRef).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID)
|
||||
doReturn(testNetworkParameters()).whenever(services).networkParameters
|
||||
|
||||
val stx = makeDummyStx()
|
||||
fun `SignedTransaction (WireTransaction)`() {
|
||||
val attachmentId = SecureHash.randomSHA256()
|
||||
doReturn(attachmentId).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID)
|
||||
val wtx = TransactionBuilder(
|
||||
notary = DUMMY_NOTARY,
|
||||
inputs = mutableListOf(StateRef(SecureHash.randomSHA256(), 1)),
|
||||
attachments = mutableListOf(attachmentId),
|
||||
outputs = mutableListOf(createTransactionState()),
|
||||
commands = mutableListOf(Command(DummyCommandData, listOf(BOB_PUBKEY))),
|
||||
window = TimeWindow.fromStartAndDuration(Instant.now(), 1.hours),
|
||||
privacySalt = net.corda.core.contracts.PrivacySalt()
|
||||
).toWireTransaction(services)
|
||||
val stx = sign(wtx)
|
||||
partyObjectMapper.identities += listOf(MINI_CORP.party, DUMMY_NOTARY)
|
||||
val json = mapper.valueToTree<ObjectNode>(stx)
|
||||
println(mapper.writeValueAsString(json))
|
||||
val (txBits, signatures) = json.assertHasOnlyFields("txBits", "signatures")
|
||||
assertThat(txBits.binaryValue()).isEqualTo(stx.txBits.bytes)
|
||||
val sigs = signatures.elements().asSequence().map { it.valueAs<TransactionSignature>(mapper) }.toList()
|
||||
assertThat(sigs).isEqualTo(stx.sigs)
|
||||
val (wtxJson, signaturesJson) = json.assertHasOnlyFields("wire", "signatures")
|
||||
assertThat(signaturesJson.childrenAs<TransactionSignature>(mapper)).isEqualTo(stx.sigs)
|
||||
val wtxFields = wtxJson.assertHasOnlyFields("id", "notary", "inputs", "attachments", "outputs", "commands", "timeWindow", "privacySalt")
|
||||
assertThat(wtxFields[0].valueAs<SecureHash>(mapper)).isEqualTo(wtx.id)
|
||||
assertThat(wtxFields[1].valueAs<Party>(mapper)).isEqualTo(wtx.notary)
|
||||
assertThat(wtxFields[2].childrenAs<StateRef>(mapper)).isEqualTo(wtx.inputs)
|
||||
assertThat(wtxFields[3].childrenAs<SecureHash>(mapper)).isEqualTo(wtx.attachments)
|
||||
assertThat(wtxFields[4].childrenAs<TransactionState<*>>(mapper)).isEqualTo(wtx.outputs)
|
||||
assertThat(wtxFields[5].childrenAs<Command<*>>(mapper)).isEqualTo(wtx.commands)
|
||||
assertThat(wtxFields[6].valueAs<TimeWindow>(mapper)).isEqualTo(wtx.timeWindow)
|
||||
assertThat(wtxFields[7].valueAs<PrivacySalt>(mapper)).isEqualTo(wtx.privacySalt)
|
||||
assertThat(mapper.convertValue<WireTransaction>(wtxJson)).isEqualTo(wtx)
|
||||
assertThat(mapper.convertValue<SignedTransaction>(json)).isEqualTo(stx)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun TransactionState() {
|
||||
val txState = createTransactionState()
|
||||
val json = mapper.valueToTree<ObjectNode>(txState)
|
||||
println(mapper.writeValueAsString(json))
|
||||
partyObjectMapper.identities += listOf(MINI_CORP.party, DUMMY_NOTARY)
|
||||
assertThat(mapper.convertValue<TransactionState<*>>(json)).isEqualTo(txState)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun Command() {
|
||||
val command = Command(DummyCommandData, listOf(BOB_PUBKEY))
|
||||
val json = mapper.valueToTree<ObjectNode>(command)
|
||||
assertThat(mapper.convertValue<Command<*>>(json)).isEqualTo(command)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `TimeWindow - fromOnly`() {
|
||||
val fromOnly = TimeWindow.fromOnly(Instant.now())
|
||||
val json = mapper.valueToTree<ObjectNode>(fromOnly)
|
||||
assertThat(mapper.convertValue<TimeWindow>(json)).isEqualTo(fromOnly)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `TimeWindow - untilOnly`() {
|
||||
val untilOnly = TimeWindow.untilOnly(Instant.now())
|
||||
val json = mapper.valueToTree<ObjectNode>(untilOnly)
|
||||
assertThat(mapper.convertValue<TimeWindow>(json)).isEqualTo(untilOnly)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `TimeWindow - between`() {
|
||||
val between = TimeWindow.between(Instant.now(), Instant.now() + 1.days)
|
||||
val json = mapper.valueToTree<ObjectNode>(between)
|
||||
assertThat(mapper.convertValue<TimeWindow>(json)).isEqualTo(between)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun PrivacySalt() {
|
||||
val privacySalt = net.corda.core.contracts.PrivacySalt()
|
||||
val json = mapper.valueToTree<TextNode>(privacySalt)
|
||||
assertThat(json.textValue()).isEqualTo(privacySalt.bytes.toHexString())
|
||||
assertThat(mapper.convertValue<PrivacySalt>(json)).isEqualTo(privacySalt)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun SignatureMetadata() {
|
||||
val signatureMetadata = SignatureMetadata(2, Crypto.ECDSA_SECP256R1_SHA256.schemeNumberID)
|
||||
val json = mapper.valueToTree<ObjectNode>(signatureMetadata)
|
||||
val (platformVersion, scheme) = json.assertHasOnlyFields("platformVersion", "scheme")
|
||||
assertThat(platformVersion.intValue()).isEqualTo(2)
|
||||
assertThat(scheme.textValue()).isEqualTo("ECDSA_SECP256R1_SHA256")
|
||||
assertThat(mapper.convertValue<SignatureMetadata>(json)).isEqualTo(signatureMetadata)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SignatureMetadata on unknown schemeNumberID`() {
|
||||
val signatureMetadata = SignatureMetadata(2, Int.MAX_VALUE)
|
||||
val json = mapper.valueToTree<ObjectNode>(signatureMetadata)
|
||||
assertThat(json["scheme"].intValue()).isEqualTo(Int.MAX_VALUE)
|
||||
assertThat(mapper.convertValue<SignatureMetadata>(json)).isEqualTo(signatureMetadata)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SignatureScheme serialization`() {
|
||||
val json = mapper.valueToTree<TextNode>(Crypto.ECDSA_SECP256R1_SHA256)
|
||||
assertThat(json.textValue()).isEqualTo("ECDSA_SECP256R1_SHA256")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SignatureScheme deserialization`() {
|
||||
assertThat(mapper.convertValue<SignatureScheme>(TextNode("EDDSA_ED25519_SHA512"))).isSameAs(Crypto.EDDSA_ED25519_SHA512)
|
||||
assertThat(mapper.convertValue<SignatureScheme>(IntNode(4))).isSameAs(Crypto.EDDSA_ED25519_SHA512)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `PartialTree IncludedLeaf`() {
|
||||
val includedLeaf = PartialTree.IncludedLeaf(SecureHash.randomSHA256())
|
||||
val json = mapper.valueToTree<ObjectNode>(includedLeaf)
|
||||
assertThat(json.assertHasOnlyFields("includedLeaf")[0].textValue()).isEqualTo(includedLeaf.hash.toString())
|
||||
assertThat(mapper.convertValue<PartialTree.IncludedLeaf>(json)).isEqualTo(includedLeaf)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `PartialTree Leaf`() {
|
||||
val leaf = PartialTree.Leaf(SecureHash.randomSHA256())
|
||||
val json = mapper.valueToTree<ObjectNode>(leaf)
|
||||
assertThat(json.assertHasOnlyFields("leaf")[0].textValue()).isEqualTo(leaf.hash.toString())
|
||||
assertThat(mapper.convertValue<PartialTree.Leaf>(json)).isEqualTo(leaf)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `simple PartialTree Node`() {
|
||||
val node = PartialTree.Node(
|
||||
left = PartialTree.Leaf(SecureHash.randomSHA256()),
|
||||
right = PartialTree.IncludedLeaf(SecureHash.randomSHA256())
|
||||
)
|
||||
val json = mapper.valueToTree<ObjectNode>(node)
|
||||
println(mapper.writeValueAsString(json))
|
||||
val (leftJson, rightJson) = json.assertHasOnlyFields("left", "right")
|
||||
assertThat(leftJson.valueAs<PartialTree>(mapper)).isEqualTo(node.left)
|
||||
assertThat(rightJson.valueAs<PartialTree>(mapper)).isEqualTo(node.right)
|
||||
assertThat(mapper.convertValue<PartialTree.Node>(json)).isEqualTo(node)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `complex PartialTree Node`() {
|
||||
val node = PartialTree.Node(
|
||||
left = PartialTree.IncludedLeaf(SecureHash.randomSHA256()),
|
||||
right = PartialTree.Node(
|
||||
left = PartialTree.Leaf(SecureHash.randomSHA256()),
|
||||
right = PartialTree.Leaf(SecureHash.randomSHA256())
|
||||
)
|
||||
)
|
||||
val json = mapper.valueToTree<ObjectNode>(node)
|
||||
println(mapper.writeValueAsString(json))
|
||||
assertThat(mapper.convertValue<PartialTree.Node>(json)).isEqualTo(node)
|
||||
}
|
||||
|
||||
// TODO Issued
|
||||
// TODO PartyAndReference
|
||||
|
||||
@Test
|
||||
fun CordaX500Name() {
|
||||
testToStringSerialisation(CordaX500Name(commonName = "COMMON", organisationUnit = "ORG UNIT", organisation = "ORG", locality = "NYC", state = "NY", country = "US"))
|
||||
testToStringSerialisation(CordaX500Name(
|
||||
commonName = "COMMON",
|
||||
organisationUnit = "ORG UNIT",
|
||||
organisation = "ORG",
|
||||
locality = "NYC",
|
||||
state = "NY",
|
||||
country = "US"
|
||||
))
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -462,14 +617,36 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
|
||||
assertThat(mapper.convertValue<NonCtorPropertiesData>(json)).isEqualTo(data)
|
||||
}
|
||||
|
||||
private fun makeDummyStx(): SignedTransaction {
|
||||
val wtx = DummyContract.generateInitial(1, DUMMY_NOTARY, MINI_CORP.ref(1))
|
||||
.toWireTransaction(services)
|
||||
@Test
|
||||
fun `kotlin object`() {
|
||||
val json = mapper.valueToTree<ObjectNode>(KotlinObject)
|
||||
assertThat(mapper.convertValue<KotlinObject>(json)).isSameAs(KotlinObject)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `@CordaSerializable kotlin object`() {
|
||||
val json = mapper.valueToTree<ObjectNode>(CordaSerializableKotlinObject)
|
||||
assertThat(mapper.convertValue<CordaSerializableKotlinObject>(json)).isSameAs(CordaSerializableKotlinObject)
|
||||
}
|
||||
|
||||
private fun sign(ctx: CoreTransaction): SignedTransaction {
|
||||
val partialMerkleTree = PartialMerkleTree(PartialTree.Node(
|
||||
left = PartialTree.Leaf(SecureHash.randomSHA256()),
|
||||
right = PartialTree.IncludedLeaf(SecureHash.randomSHA256())
|
||||
))
|
||||
val signatures = listOf(
|
||||
TransactionSignature(ByteArray(1), ALICE_PUBKEY, SignatureMetadata(1, Crypto.findSignatureScheme(ALICE_PUBKEY).schemeNumberID)),
|
||||
TransactionSignature(ByteArray(1), ALICE_PUBKEY, SignatureMetadata(1, Crypto.findSignatureScheme(ALICE_PUBKEY).schemeNumberID), partialMerkleTree),
|
||||
TransactionSignature(ByteArray(1), BOB_PUBKEY, SignatureMetadata(1, Crypto.findSignatureScheme(BOB_PUBKEY).schemeNumberID))
|
||||
)
|
||||
return SignedTransaction(wtx, signatures)
|
||||
return SignedTransaction(ctx, signatures)
|
||||
}
|
||||
|
||||
private fun createTransactionState(): TransactionState<DummyContract.SingleOwnerState> {
|
||||
return TransactionState(
|
||||
data = DummyContract.SingleOwnerState(magicNumber = 123, owner = MINI_CORP.party),
|
||||
contract = DummyContract.PROGRAM_ID,
|
||||
notary = DUMMY_NOTARY
|
||||
)
|
||||
}
|
||||
|
||||
private inline fun <reified T : Any> testToStringSerialisation(value: T) {
|
||||
@ -495,6 +672,11 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
|
||||
val nonCtor: Int get() = value
|
||||
}
|
||||
|
||||
private object KotlinObject
|
||||
|
||||
@CordaSerializable
|
||||
private object CordaSerializableKotlinObject
|
||||
|
||||
private class TestPartyObjectMapper : JacksonSupport.PartyObjectMapper {
|
||||
override var isFullParties: Boolean = false
|
||||
val identities = ArrayList<Party>()
|
||||
|
@ -220,7 +220,7 @@ object Crypto {
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory pattern to retrieve the corresponding [SignatureScheme] based on the type of the [String] input.
|
||||
* Factory pattern to retrieve the corresponding [SignatureScheme] based on [SignatureScheme.schemeCodeName].
|
||||
* This function is usually called by key generators and verify signature functions.
|
||||
* In case the input is not a key in the supportedSignatureSchemes map, null will be returned.
|
||||
* @param schemeCodeName a [String] that should match a supported signature scheme code name (e.g. ECDSA_SECP256K1_SHA256), see [Crypto].
|
||||
|
@ -6,8 +6,9 @@ import java.security.spec.AlgorithmParameterSpec
|
||||
|
||||
/**
|
||||
* This class is used to define a digital signature scheme.
|
||||
* @param schemeNumberID we assign a number ID for better efficiency on-wire serialisation. Please ensure uniqueness between schemes.
|
||||
* @param schemeCodeName code name for this signature scheme (e.g. RSA_SHA256, ECDSA_SECP256K1_SHA256, ECDSA_SECP256R1_SHA256, EDDSA_ED25519_SHA512, SPHINCS-256_SHA512).
|
||||
* @param schemeNumberID unique number ID for better efficiency on-wire serialisation.
|
||||
* @param schemeCodeName unique code name for this signature scheme (e.g. RSA_SHA256, ECDSA_SECP256K1_SHA256, ECDSA_SECP256R1_SHA256,
|
||||
* EDDSA_ED25519_SHA512, SPHINCS-256_SHA512).
|
||||
* @param signatureOID ASN.1 algorithm identifier of the signature algorithm (e.g 1.3.101.112 for EdDSA)
|
||||
* @param alternativeOIDs ASN.1 algorithm identifiers for keys of the signature, where we want to map multiple keys to
|
||||
* the same signature scheme.
|
||||
|
@ -7,12 +7,7 @@ import com.google.common.hash.HashingInputStream
|
||||
import net.corda.core.cordapp.Cordapp
|
||||
import net.corda.core.cordapp.CordappConfig
|
||||
import net.corda.core.cordapp.CordappContext
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.SignedData
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.crypto.sign
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
@ -37,6 +32,7 @@ import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.Member
|
||||
import java.lang.reflect.Modifier
|
||||
import java.math.BigDecimal
|
||||
import java.net.HttpURLConnection
|
||||
@ -51,23 +47,11 @@ import java.nio.file.Paths
|
||||
import java.security.KeyPair
|
||||
import java.security.PrivateKey
|
||||
import java.security.PublicKey
|
||||
import java.security.cert.CertPath
|
||||
import java.security.cert.CertPathValidator
|
||||
import java.security.cert.CertPathValidatorException
|
||||
import java.security.cert.PKIXCertPathValidatorResult
|
||||
import java.security.cert.PKIXParameters
|
||||
import java.security.cert.TrustAnchor
|
||||
import java.security.cert.X509Certificate
|
||||
import java.security.cert.*
|
||||
import java.time.Duration
|
||||
import java.time.temporal.Temporal
|
||||
import java.util.*
|
||||
import java.util.Spliterator.DISTINCT
|
||||
import java.util.Spliterator.IMMUTABLE
|
||||
import java.util.Spliterator.NONNULL
|
||||
import java.util.Spliterator.ORDERED
|
||||
import java.util.Spliterator.SIZED
|
||||
import java.util.Spliterator.SORTED
|
||||
import java.util.Spliterator.SUBSIZED
|
||||
import java.util.Spliterator.*
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.stream.IntStream
|
||||
@ -311,6 +295,23 @@ fun <T : Any> KClass<T>.objectOrNewInstance(): T {
|
||||
return this.objectInstance ?: this.createInstance()
|
||||
}
|
||||
|
||||
/** Similar to [KClass.objectInstance] but also works on private objects. */
|
||||
val <T : Any> Class<T>.kotlinObjectInstance: T? get() {
|
||||
return try {
|
||||
kotlin.objectInstance
|
||||
} catch (_: Throwable) {
|
||||
val field = try { getDeclaredField("INSTANCE") } catch (_: NoSuchFieldException) { null }
|
||||
field?.let {
|
||||
if (it.type == this && it.isPublic && it.isStatic && it.isFinal) {
|
||||
it.isAccessible = true
|
||||
uncheckedCast(it.get(null))
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple wrapper around a [Field] object providing type safe read and write access using [value], ignoring the field's
|
||||
* visibility.
|
||||
@ -385,6 +386,12 @@ inline val Class<*>.isAbstractClass: Boolean get() = Modifier.isAbstract(modifie
|
||||
|
||||
inline val Class<*>.isConcreteClass: Boolean get() = !isInterface && !isAbstractClass
|
||||
|
||||
inline val Member.isPublic: Boolean get() = Modifier.isPublic(modifiers)
|
||||
|
||||
inline val Member.isStatic: Boolean get() = Modifier.isStatic(modifiers)
|
||||
|
||||
inline val Member.isFinal: Boolean get() = Modifier.isFinal(modifiers)
|
||||
|
||||
fun URI.toPath(): Path = Paths.get(this)
|
||||
|
||||
fun URL.toPath(): Path = toURI().toPath()
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.core.internal
|
||||
|
||||
import org.assertj.core.api.Assertions
|
||||
import net.corda.core.contracts.TimeWindow
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Test
|
||||
import java.util.stream.IntStream
|
||||
@ -8,29 +9,29 @@ import java.util.stream.Stream
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class InternalUtilsTest {
|
||||
open class InternalUtilsTest {
|
||||
@Test
|
||||
fun `noneOrSingle on an empty collection`() {
|
||||
val collection = emptyList<Int>()
|
||||
Assertions.assertThat(collection.noneOrSingle()).isNull()
|
||||
Assertions.assertThat(collection.noneOrSingle { it == 1 }).isNull()
|
||||
assertThat(collection.noneOrSingle()).isNull()
|
||||
assertThat(collection.noneOrSingle { it == 1 }).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `noneOrSingle on a singleton collection`() {
|
||||
val collection = listOf(1)
|
||||
Assertions.assertThat(collection.noneOrSingle()).isEqualTo(1)
|
||||
Assertions.assertThat(collection.noneOrSingle { it == 1 }).isEqualTo(1)
|
||||
Assertions.assertThat(collection.noneOrSingle { it == 2 }).isNull()
|
||||
assertThat(collection.noneOrSingle()).isEqualTo(1)
|
||||
assertThat(collection.noneOrSingle { it == 1 }).isEqualTo(1)
|
||||
assertThat(collection.noneOrSingle { it == 2 }).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `noneOrSingle on a collection with two items`() {
|
||||
val collection = listOf(1, 2)
|
||||
assertFailsWith<IllegalArgumentException> { collection.noneOrSingle() }
|
||||
Assertions.assertThat(collection.noneOrSingle { it == 1 }).isEqualTo(1)
|
||||
Assertions.assertThat(collection.noneOrSingle { it == 2 }).isEqualTo(2)
|
||||
Assertions.assertThat(collection.noneOrSingle { it == 3 }).isNull()
|
||||
assertThat(collection.noneOrSingle { it == 1 }).isEqualTo(1)
|
||||
assertThat(collection.noneOrSingle { it == 2 }).isEqualTo(2)
|
||||
assertThat(collection.noneOrSingle { it == 3 }).isNull()
|
||||
assertFailsWith<IllegalArgumentException> { collection.noneOrSingle { it > 0 } }
|
||||
}
|
||||
|
||||
@ -39,7 +40,7 @@ class InternalUtilsTest {
|
||||
val collection = listOf(1, 2, 1)
|
||||
assertFailsWith<IllegalArgumentException> { collection.noneOrSingle() }
|
||||
assertFailsWith<IllegalArgumentException> { collection.noneOrSingle { it == 1 } }
|
||||
Assertions.assertThat(collection.noneOrSingle { it == 2 }).isEqualTo(2)
|
||||
assertThat(collection.noneOrSingle { it == 2 }).isEqualTo(2)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -88,4 +89,19 @@ class InternalUtilsTest {
|
||||
assertEquals(Array<String?>::class.java, b.javaClass)
|
||||
assertArrayEquals(arrayOf("one", "two", null), b)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun kotlinObjectInstance() {
|
||||
assertThat(PublicObject::class.java.kotlinObjectInstance).isSameAs(PublicObject)
|
||||
assertThat(PrivateObject::class.java.kotlinObjectInstance).isSameAs(PrivateObject)
|
||||
assertThat(ProtectedObject::class.java.kotlinObjectInstance).isSameAs(ProtectedObject)
|
||||
assertThat(TimeWindow::class.java.kotlinObjectInstance).isNull()
|
||||
assertThat(PrivateClass::class.java.kotlinObjectInstance).isNull()
|
||||
}
|
||||
|
||||
object PublicObject
|
||||
private object PrivateObject
|
||||
protected object ProtectedObject
|
||||
|
||||
private class PrivateClass
|
||||
}
|
||||
|
@ -51,7 +51,10 @@ Unreleased
|
||||
The encoded bytes are also serialised into the ``encoded`` field. This can be used to deserialise an ``X509Certificate``
|
||||
back.
|
||||
* ``CertPath`` objects are serialised as a list of ``X509Certificate`` objects.
|
||||
* ``SignedTransaction`` is serialised into its ``txBits`` and ``signatures`` and can be deserialised back
|
||||
* ``WireTransaction`` now nicely outputs into its components: ``id``, ``notary``, ``inputs``, ``attachments``, ``outputs``,
|
||||
``commands``, ``timeWindow`` and ``privacySalt``. This can be deserialised back.
|
||||
* ``SignedTransaction`` is serialised into ``wire`` (i.e. currently only ``WireTransaction`` tested) and ``signatures``,
|
||||
and can be deserialised back.
|
||||
|
||||
* ``fullParties`` boolean parameter added to ``JacksonSupport.createDefaultMapper`` and ``createNonRpcMapper``. If ``true``
|
||||
then ``Party`` objects are serialised as JSON objects with the ``name`` and ``owningKey`` fields. For ``PartyAndCertificate``
|
||||
|
@ -2,13 +2,9 @@
|
||||
|
||||
package net.corda.nodeapi.internal.config
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigException
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import com.typesafe.config.ConfigUtil
|
||||
import com.typesafe.config.ConfigValueFactory
|
||||
import com.typesafe.config.ConfigValueType
|
||||
import com.typesafe.config.*
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.isStatic
|
||||
import net.corda.core.internal.noneOrSingle
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
@ -16,7 +12,6 @@ import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.lang.reflect.Modifier.isStatic
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import java.net.Proxy
|
||||
import java.net.URL
|
||||
@ -209,7 +204,7 @@ fun Any.toConfig(): Config = ConfigValueFactory.fromMap(toConfigMap()).toConfig(
|
||||
private fun Any.toConfigMap(): Map<String, Any> {
|
||||
val values = HashMap<String, Any>()
|
||||
for (field in javaClass.declaredFields) {
|
||||
if (isStatic(field.modifiers) || field.isSynthetic) continue
|
||||
if (field.isStatic || field.isSynthetic) continue
|
||||
field.isAccessible = true
|
||||
val value = field.get(this) ?: continue
|
||||
val configValue = if (value is String || value is Boolean || value is Number) {
|
||||
|
@ -6,6 +6,7 @@ import com.esotericsoftware.kryo.io.Output
|
||||
import com.esotericsoftware.kryo.serializers.FieldSerializer
|
||||
import com.esotericsoftware.kryo.util.DefaultClassResolver
|
||||
import com.esotericsoftware.kryo.util.Util
|
||||
import net.corda.core.internal.kotlinObjectInstance
|
||||
import net.corda.core.internal.writer
|
||||
import net.corda.core.serialization.ClassWhitelist
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
@ -15,7 +16,6 @@ import net.corda.serialization.internal.MutableClassWhitelist
|
||||
import net.corda.serialization.internal.TransientClassWhiteList
|
||||
import net.corda.serialization.internal.amqp.hasCordaSerializable
|
||||
import java.io.PrintWriter
|
||||
import java.lang.reflect.Modifier
|
||||
import java.lang.reflect.Modifier.isAbstract
|
||||
import java.nio.charset.StandardCharsets.UTF_8
|
||||
import java.nio.file.Paths
|
||||
@ -75,22 +75,7 @@ class CordaClassResolver(serializationContext: SerializationContext) : DefaultCl
|
||||
|
||||
override fun registerImplicit(type: Class<*>): Registration {
|
||||
val targetType = typeForSerializationOf(type)
|
||||
// Is this a Kotlin object? We use our own reflection here rather than .kotlin.objectInstance because Kotlin
|
||||
// reflection won't work for private objects, and can throw exceptions in other circumstances as well.
|
||||
val objectInstance = try {
|
||||
targetType.declaredFields.singleOrNull {
|
||||
it.name == "INSTANCE" &&
|
||||
it.type == type &&
|
||||
Modifier.isStatic(it.modifiers) &&
|
||||
Modifier.isFinal(it.modifiers) &&
|
||||
Modifier.isPublic(it.modifiers)
|
||||
}?.let {
|
||||
it.isAccessible = true
|
||||
type.cast(it.get(null)!!)
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
null
|
||||
}
|
||||
val objectInstance = targetType.kotlinObjectInstance
|
||||
|
||||
// We have to set reference to true, since the flag influences how String fields are treated and we want it to be consistent.
|
||||
val references = kryo.references
|
||||
|
@ -1,7 +1,7 @@
|
||||
package net.corda.node.utilities
|
||||
|
||||
import net.corda.core.internal.isStatic
|
||||
import java.lang.reflect.Method
|
||||
import java.lang.reflect.Modifier
|
||||
import java.lang.reflect.Type
|
||||
import java.time.Instant
|
||||
|
||||
@ -131,7 +131,7 @@ object ObjectDiffer {
|
||||
private fun getFieldFoci(obj: Any) : List<FieldFocus> {
|
||||
val foci = ArrayList<FieldFocus>()
|
||||
for (method in obj.javaClass.declaredMethods) {
|
||||
if (Modifier.isStatic(method.modifiers)) {
|
||||
if (method.isStatic) {
|
||||
continue
|
||||
}
|
||||
if (method.name.startsWith("get") && method.name.length > 3 && method.parameterCount == 0) {
|
||||
|
@ -2,6 +2,7 @@ package net.corda.serialization.internal.amqp
|
||||
|
||||
import com.google.common.hash.Hasher
|
||||
import com.google.common.hash.Hashing
|
||||
import net.corda.core.internal.kotlinObjectInstance
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.core.utilities.toBase64
|
||||
import java.io.NotSerializableException
|
||||
@ -153,7 +154,7 @@ class SerializerFingerPrinter : FingerPrinter {
|
||||
}.putUnencodedChars(type.name).putUnencodedChars(ENUM_HASH)
|
||||
} else {
|
||||
hasher.fingerprintWithCustomSerializerOrElse(factory!!, type, type) {
|
||||
if (type.objectInstance() != null) {
|
||||
if (type.kotlinObjectInstance != null) {
|
||||
// TODO: name collision is too likely for kotlin objects, we need to introduce some reference
|
||||
// to the CorDapp but maybe reference to the JAR in the short term.
|
||||
hasher.putUnencodedChars(type.name)
|
||||
|
@ -3,6 +3,7 @@ package net.corda.serialization.internal.amqp
|
||||
import com.google.common.primitives.Primitives
|
||||
import com.google.common.reflect.TypeToken
|
||||
import net.corda.core.internal.isConcreteClass
|
||||
import net.corda.core.internal.isPublic
|
||||
import net.corda.core.serialization.ClassWhitelist
|
||||
import net.corda.core.serialization.ConstructorForDeserialization
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
@ -155,7 +156,7 @@ fun Class<out Any?>.propertyDescriptors(): Map<String, PropertyDescriptor> {
|
||||
// In addition, only getters that take zero parameters and setters that take a single
|
||||
// parameter will be considered
|
||||
clazz!!.declaredMethods?.map { func ->
|
||||
if (!Modifier.isPublic(func.modifiers)) return@map
|
||||
if (!func.isPublic) return@map
|
||||
if (func.name == "getClass") return@map
|
||||
|
||||
PropertyDescriptorsRegex.re.find(func.name)?.apply {
|
||||
@ -537,49 +538,3 @@ fun hasCordaSerializable(type: Class<*>): Boolean {
|
||||
|| type.interfaces.any(::hasCordaSerializable)
|
||||
|| (type.superclass != null && hasCordaSerializable(type.superclass))
|
||||
}
|
||||
|
||||
/**
|
||||
* By default use Kotlin reflection and grab the objectInstance. However, that doesn't play nicely with nested
|
||||
* private objects. Even setting the accessibility override (setAccessible) still causes an
|
||||
* [IllegalAccessException] when attempting to retrieve the value of the INSTANCE field.
|
||||
*
|
||||
* Whichever reference to the class Kotlin reflection uses, override (set from setAccessible) on that field
|
||||
* isn't set even when it was explicitly set as accessible before calling into the kotlin reflection routines.
|
||||
*
|
||||
* For example
|
||||
*
|
||||
* clazz.getDeclaredField("INSTANCE")?.apply {
|
||||
* isAccessible = true
|
||||
* kotlin.objectInstance // This throws as the INSTANCE field isn't accessible
|
||||
* }
|
||||
*
|
||||
* Therefore default back to good old java reflection and simply look for the INSTANCE field as we are never going
|
||||
* to serialize a companion object.
|
||||
*
|
||||
* As such, if objectInstance fails access, revert to Java reflection and try that
|
||||
*/
|
||||
fun Class<*>.objectInstance(): Any? {
|
||||
return try {
|
||||
this.kotlin.objectInstance
|
||||
} catch (e: IllegalAccessException) {
|
||||
// Check it really is an object (i.e. it has no constructor)
|
||||
if (constructors.isNotEmpty()) null
|
||||
else {
|
||||
try {
|
||||
this.getDeclaredField("INSTANCE")?.let { field ->
|
||||
// and must be marked as both static and final (>0 means they're set)
|
||||
if (modifiers and Modifier.STATIC == 0 || modifiers and Modifier.FINAL == 0) null
|
||||
else {
|
||||
val accessibility = field.isAccessible
|
||||
field.isAccessible = true
|
||||
val obj = field.get(null)
|
||||
field.isAccessible = accessibility
|
||||
obj
|
||||
}
|
||||
}
|
||||
} catch (e: NoSuchFieldException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package net.corda.serialization.internal.amqp
|
||||
|
||||
import com.google.common.primitives.Primitives
|
||||
import com.google.common.reflect.TypeResolver
|
||||
import net.corda.core.internal.kotlinObjectInstance
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.serialization.ClassWhitelist
|
||||
import net.corda.core.utilities.debug
|
||||
@ -329,7 +330,7 @@ open class SerializerFactory(
|
||||
if (clazz.componentType.isPrimitive) PrimArraySerializer.make(type, this)
|
||||
else ArraySerializer.make(type, this)
|
||||
} else {
|
||||
val singleton = clazz.objectInstance()
|
||||
val singleton = clazz.kotlinObjectInstance
|
||||
if (singleton != null) {
|
||||
whitelist.requireWhitelisted(clazz)
|
||||
SingletonSerializer(clazz, singleton, this)
|
||||
|
Loading…
Reference in New Issue
Block a user