mirror of
https://github.com/corda/corda.git
synced 2024-12-19 21:17:58 +00:00
ENT-6357: Deserialize LedgerTransaction elements for each Contract.verify(). (#6962)
* ENT-6357: Deserialize LedgerTransaction elements for each Contract.verify(). * Lock the LedgerTransaction and NetworkParameters objects down for contract verification. * Refactor BasicVerifier to be package private instead of public. * Simplify verifyConstraints() operation. * Review fixes: replace HashSet with LinkedHashSet, and add signing parties to commands via mapIndexed. * Ensure tests also run notary nodes "out of process". * Streamline SerializationContext switching. * Cache deserialised cryptographic instances during contract verification. * Invoke Class.forName() instead of ClassLoader.loadClass() to reduce contention on the system classloader's lock. * Deserialization cache key now pre-computes its hash code. * Allow AttachmentsClassLoader to be used concurrently. * Cache all Envelope objects for reuse during contract verification. * Generate CertPathProxy hash code using conventional algorithm. * Adjust CustomSerializer.Proxy to allow better access to SerializationContext.
This commit is contained in:
parent
b0889e0201
commit
caeafb8201
@ -23,7 +23,10 @@ def javaHome = System.getProperty('java.home')
|
|||||||
def jarBaseName = "corda-${project.name}".toString()
|
def jarBaseName = "corda-${project.name}".toString()
|
||||||
|
|
||||||
configurations {
|
configurations {
|
||||||
deterministicLibraries.extendsFrom api
|
deterministicLibraries {
|
||||||
|
canBeConsumed = false
|
||||||
|
extendsFrom api
|
||||||
|
}
|
||||||
deterministicArtifacts.extendsFrom deterministicLibraries
|
deterministicArtifacts.extendsFrom deterministicLibraries
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,7 +62,7 @@ def originalJar = coreJarTask.map { it.outputs.files.singleFile }
|
|||||||
|
|
||||||
def patchCore = tasks.register('patchCore', Zip) {
|
def patchCore = tasks.register('patchCore', Zip) {
|
||||||
dependsOn coreJarTask
|
dependsOn coreJarTask
|
||||||
destinationDirectory = file("$buildDir/source-libs")
|
destinationDirectory = layout.buildDirectory.dir('source-libs')
|
||||||
metadataCharset 'UTF-8'
|
metadataCharset 'UTF-8'
|
||||||
archiveClassifier = 'transient'
|
archiveClassifier = 'transient'
|
||||||
archiveExtension = 'jar'
|
archiveExtension = 'jar'
|
||||||
@ -169,7 +172,7 @@ def determinise = tasks.register('determinise', ProGuardTask) {
|
|||||||
def checkDeterminism = tasks.register('checkDeterminism', ProGuardTask)
|
def checkDeterminism = tasks.register('checkDeterminism', ProGuardTask)
|
||||||
|
|
||||||
def metafix = tasks.register('metafix', MetaFixerTask) {
|
def metafix = tasks.register('metafix', MetaFixerTask) {
|
||||||
outputDir file("$buildDir/libs")
|
outputDir = layout.buildDirectory.dir('libs')
|
||||||
jars determinise
|
jars determinise
|
||||||
suffix ""
|
suffix ""
|
||||||
|
|
||||||
|
@ -55,12 +55,16 @@ abstract class SerializationFactory {
|
|||||||
* Change the current context inside the block to that supplied.
|
* Change the current context inside the block to that supplied.
|
||||||
*/
|
*/
|
||||||
fun <T> withCurrentContext(context: SerializationContext?, block: () -> T): T {
|
fun <T> withCurrentContext(context: SerializationContext?, block: () -> T): T {
|
||||||
val priorContext = _currentContext
|
return if (context == null) {
|
||||||
if (context != null) _currentContext = context
|
block()
|
||||||
try {
|
} else {
|
||||||
return block()
|
val priorContext = _currentContext
|
||||||
} finally {
|
_currentContext = context
|
||||||
if (context != null) _currentContext = priorContext
|
try {
|
||||||
|
block()
|
||||||
|
} finally {
|
||||||
|
_currentContext = priorContext
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,9 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
configurations {
|
configurations {
|
||||||
testData
|
testData {
|
||||||
|
canBeResolved = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
@ -9,7 +9,12 @@ apply from: "${rootProject.projectDir}/deterministic.gradle"
|
|||||||
description 'Test utilities for deterministic contract verification'
|
description 'Test utilities for deterministic contract verification'
|
||||||
|
|
||||||
configurations {
|
configurations {
|
||||||
deterministicArtifacts
|
deterministicArtifacts {
|
||||||
|
canBeResolved = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile against the deterministic artifacts to ensure that we use only the deterministic API subset.
|
||||||
|
compileOnly.extendsFrom deterministicArtifacts
|
||||||
runtimeArtifacts.extendsFrom api
|
runtimeArtifacts.extendsFrom api
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,8 +25,6 @@ dependencies {
|
|||||||
runtimeArtifacts project(':serialization')
|
runtimeArtifacts project(':serialization')
|
||||||
runtimeArtifacts project(':core')
|
runtimeArtifacts project(':core')
|
||||||
|
|
||||||
// Compile against the deterministic artifacts to ensure that we use only the deterministic API subset.
|
|
||||||
compileOnly configurations.deterministicArtifacts
|
|
||||||
api "junit:junit:$junit_version"
|
api "junit:junit:$junit_version"
|
||||||
runtimeOnly "org.junit.vintage:junit-vintage-engine:$junit_vintage_version"
|
runtimeOnly "org.junit.vintage:junit-vintage-engine:$junit_vintage_version"
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ import net.corda.core.transactions.WireTransaction
|
|||||||
|
|
||||||
@Suppress("MemberVisibilityCanBePrivate")
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
//TODO the use of deprecated toLedgerTransaction need to be revisited as resolveContractAttachment requires attachments of the transactions which created input states...
|
//TODO the use of deprecated toLedgerTransaction need to be revisited as resolveContractAttachment requires attachments of the transactions which created input states...
|
||||||
//TODO ...to check contract version non downgrade rule, curretly dummy Attachment if not fund is used which sets contract version to '1'
|
//TODO ...to check contract version non downgrade rule, currently dummy Attachment if not fund is used which sets contract version to '1'
|
||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
class TransactionVerificationRequest(val wtxToVerify: SerializedBytes<WireTransaction>,
|
class TransactionVerificationRequest(val wtxToVerify: SerializedBytes<WireTransaction>,
|
||||||
val dependencies: Array<SerializedBytes<WireTransaction>>,
|
val dependencies: Array<SerializedBytes<WireTransaction>>,
|
||||||
|
@ -55,7 +55,8 @@ class AttachmentsClassLoaderSerializationTests {
|
|||||||
arrayOf(isolatedId, att1, att2).map { storage.openAttachment(it)!! },
|
arrayOf(isolatedId, att1, att2).map { storage.openAttachment(it)!! },
|
||||||
testNetworkParameters(),
|
testNetworkParameters(),
|
||||||
SecureHash.zeroHash,
|
SecureHash.zeroHash,
|
||||||
{ attachmentTrustCalculator.calculate(it) }, attachmentsClassLoaderCache = null) { classLoader ->
|
{ attachmentTrustCalculator.calculate(it) }, attachmentsClassLoaderCache = null) { serializationContext ->
|
||||||
|
val classLoader = serializationContext.deserializationClassLoader
|
||||||
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, classLoader)
|
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, classLoader)
|
||||||
val contract = contractClass.getDeclaredConstructor().newInstance() as Contract
|
val contract = contractClass.getDeclaredConstructor().newInstance() as Contract
|
||||||
assertEquals("helloworld", contract.declaredField<Any?>("magicString").value)
|
assertEquals("helloworld", contract.declaredField<Any?>("magicString").value)
|
||||||
|
@ -28,9 +28,7 @@ sealed class DigestAlgorithmFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private class CustomAlgorithmFactory(className: String) : DigestAlgorithmFactory() {
|
private class CustomAlgorithmFactory(className: String) : DigestAlgorithmFactory() {
|
||||||
val constructor: Constructor<out DigestAlgorithm> = javaClass
|
val constructor: Constructor<out DigestAlgorithm> = Class.forName(className, false, javaClass.classLoader)
|
||||||
.classLoader
|
|
||||||
.loadClass(className)
|
|
||||||
.asSubclass(DigestAlgorithm::class.java)
|
.asSubclass(DigestAlgorithm::class.java)
|
||||||
.getConstructor()
|
.getConstructor()
|
||||||
override val algorithm: String = constructor.newInstance().algorithm
|
override val algorithm: String = constructor.newInstance().algorithm
|
||||||
|
@ -23,7 +23,7 @@ import net.corda.core.serialization.internal.AttachmentURLStreamHandlerFactory.a
|
|||||||
fun <T: Any> createInstancesOfClassesImplementing(classloader: ClassLoader, clazz: Class<T>,
|
fun <T: Any> createInstancesOfClassesImplementing(classloader: ClassLoader, clazz: Class<T>,
|
||||||
classVersionRange: IntRange? = null): Set<T> {
|
classVersionRange: IntRange? = null): Set<T> {
|
||||||
return getNamesOfClassesImplementing(classloader, clazz, classVersionRange)
|
return getNamesOfClassesImplementing(classloader, clazz, classVersionRange)
|
||||||
.map { classloader.loadClass(it).asSubclass(clazz) }
|
.map { Class.forName(it, false, classloader).asSubclass(clazz) }
|
||||||
.mapTo(LinkedHashSet()) { it.kotlin.objectOrNewInstance() }
|
.mapTo(LinkedHashSet()) { it.kotlin.objectOrNewInstance() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ fun combinedHash(components: Iterable<SecureHash>, digestService: DigestService)
|
|||||||
components.forEach {
|
components.forEach {
|
||||||
stream.write(it.bytes)
|
stream.write(it.bytes)
|
||||||
}
|
}
|
||||||
return digestService.hash(stream.toByteArray());
|
return digestService.hash(stream.toByteArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -3,14 +3,39 @@ package net.corda.core.internal
|
|||||||
import net.corda.core.DeleteForDJVM
|
import net.corda.core.DeleteForDJVM
|
||||||
import net.corda.core.KeepForDJVM
|
import net.corda.core.KeepForDJVM
|
||||||
import net.corda.core.concurrent.CordaFuture
|
import net.corda.core.concurrent.CordaFuture
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.Attachment
|
||||||
|
import net.corda.core.contracts.Contract
|
||||||
|
import net.corda.core.contracts.ContractAttachment
|
||||||
|
import net.corda.core.contracts.ContractClassName
|
||||||
|
import net.corda.core.contracts.ContractState
|
||||||
|
import net.corda.core.contracts.HashAttachmentConstraint
|
||||||
|
import net.corda.core.contracts.SignatureAttachmentConstraint
|
||||||
|
import net.corda.core.contracts.StateAndRef
|
||||||
|
import net.corda.core.contracts.StateRef
|
||||||
|
import net.corda.core.contracts.TransactionState
|
||||||
|
import net.corda.core.contracts.TransactionVerificationException.ConflictingAttachmentsRejection
|
||||||
|
import net.corda.core.contracts.TransactionVerificationException.ConstraintPropagationRejection
|
||||||
|
import net.corda.core.contracts.TransactionVerificationException.ContractCreationError
|
||||||
|
import net.corda.core.contracts.TransactionVerificationException.ContractRejection
|
||||||
|
import net.corda.core.contracts.TransactionVerificationException.ContractConstraintRejection
|
||||||
|
import net.corda.core.contracts.TransactionVerificationException.Direction
|
||||||
|
import net.corda.core.contracts.TransactionVerificationException.DuplicateAttachmentsRejection
|
||||||
|
import net.corda.core.contracts.TransactionVerificationException.InvalidConstraintRejection
|
||||||
|
import net.corda.core.contracts.TransactionVerificationException.MissingAttachmentRejection
|
||||||
|
import net.corda.core.contracts.TransactionVerificationException.NotaryChangeInWrongTransactionType
|
||||||
import net.corda.core.contracts.TransactionVerificationException.TransactionContractConflictException
|
import net.corda.core.contracts.TransactionVerificationException.TransactionContractConflictException
|
||||||
|
import net.corda.core.contracts.TransactionVerificationException.TransactionDuplicateEncumbranceException
|
||||||
|
import net.corda.core.contracts.TransactionVerificationException.TransactionMissingEncumbranceException
|
||||||
|
import net.corda.core.contracts.TransactionVerificationException.TransactionNonMatchingEncumbranceException
|
||||||
|
import net.corda.core.contracts.TransactionVerificationException.TransactionNotaryMismatchEncumbranceException
|
||||||
|
import net.corda.core.contracts.TransactionVerificationException.TransactionRequiredContractUnspecifiedException
|
||||||
import net.corda.core.crypto.CompositeKey
|
import net.corda.core.crypto.CompositeKey
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.internal.rules.StateContractValidationEnforcementRule
|
import net.corda.core.internal.rules.StateContractValidationEnforcementRule
|
||||||
import net.corda.core.transactions.LedgerTransaction
|
import net.corda.core.transactions.LedgerTransaction
|
||||||
import net.corda.core.utilities.contextLogger
|
import net.corda.core.utilities.contextLogger
|
||||||
import java.util.function.Function
|
import java.util.function.Function
|
||||||
|
import java.util.function.Supplier
|
||||||
|
|
||||||
@DeleteForDJVM
|
@DeleteForDJVM
|
||||||
interface TransactionVerifierServiceInternal {
|
interface TransactionVerifierServiceInternal {
|
||||||
@ -27,8 +52,8 @@ fun LedgerTransaction.prepareVerify(attachments: List<Attachment>) = internalPre
|
|||||||
* wrong object instance. This class helps avoid that.
|
* wrong object instance. This class helps avoid that.
|
||||||
*/
|
*/
|
||||||
abstract class Verifier(val ltx: LedgerTransaction, protected val transactionClassLoader: ClassLoader) {
|
abstract class Verifier(val ltx: LedgerTransaction, protected val transactionClassLoader: ClassLoader) {
|
||||||
private val inputStates: List<TransactionState<*>> = ltx.inputs.map { it.state }
|
private val inputStates: List<TransactionState<*>> = ltx.inputs.map(StateAndRef<ContractState>::state)
|
||||||
private val allStates: List<TransactionState<*>> = inputStates + ltx.references.map { it.state } + ltx.outputs
|
private val allStates: List<TransactionState<*>> = inputStates + ltx.references.map(StateAndRef<ContractState>::state) + ltx.outputs
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val logger = contextLogger()
|
val logger = contextLogger()
|
||||||
@ -39,7 +64,7 @@ abstract class Verifier(val ltx: LedgerTransaction, protected val transactionCla
|
|||||||
*
|
*
|
||||||
* It is a critical piece of the security of the platform.
|
* It is a critical piece of the security of the platform.
|
||||||
*
|
*
|
||||||
* @throws TransactionVerificationException
|
* @throws net.corda.core.contracts.TransactionVerificationException
|
||||||
*/
|
*/
|
||||||
fun verify() {
|
fun verify() {
|
||||||
// checkNoNotaryChange and checkEncumbrancesValid are called here, and not in the c'tor, as they need access to the "outputs"
|
// checkNoNotaryChange and checkEncumbrancesValid are called here, and not in the c'tor, as they need access to the "outputs"
|
||||||
@ -82,10 +107,10 @@ abstract class Verifier(val ltx: LedgerTransaction, protected val transactionCla
|
|||||||
* This is an important piece of the security of transactions.
|
* This is an important piece of the security of transactions.
|
||||||
*/
|
*/
|
||||||
private fun getUniqueContractAttachmentsByContract(): Map<ContractClassName, ContractAttachment> {
|
private fun getUniqueContractAttachmentsByContract(): Map<ContractClassName, ContractAttachment> {
|
||||||
val contractClasses = allStates.map { it.contract }.toSet()
|
val contractClasses = allStates.mapTo(LinkedHashSet(), TransactionState<*>::contract)
|
||||||
|
|
||||||
// Check that there are no duplicate attachments added.
|
// Check that there are no duplicate attachments added.
|
||||||
if (ltx.attachments.size != ltx.attachments.toSet().size) throw TransactionVerificationException.DuplicateAttachmentsRejection(ltx.id, ltx.attachments.groupBy { it }.filterValues { it.size > 1 }.keys.first())
|
if (ltx.attachments.size != ltx.attachments.toSet().size) throw DuplicateAttachmentsRejection(ltx.id, ltx.attachments.groupBy { it }.filterValues { it.size > 1 }.keys.first())
|
||||||
|
|
||||||
// For each attachment this finds all the relevant state contracts that it provides.
|
// For each attachment this finds all the relevant state contracts that it provides.
|
||||||
// And then maps them to the attachment.
|
// And then maps them to the attachment.
|
||||||
@ -103,12 +128,12 @@ abstract class Verifier(val ltx: LedgerTransaction, protected val transactionCla
|
|||||||
.groupBy { it.first } // Group by contract.
|
.groupBy { it.first } // Group by contract.
|
||||||
.filter { (_, attachments) -> attachments.size > 1 } // And only keep contracts that are in multiple attachments. It's guaranteed that attachments were unique by a previous check.
|
.filter { (_, attachments) -> attachments.size > 1 } // And only keep contracts that are in multiple attachments. It's guaranteed that attachments were unique by a previous check.
|
||||||
.keys.firstOrNull() // keep the first one - if any - to throw a meaningful exception.
|
.keys.firstOrNull() // keep the first one - if any - to throw a meaningful exception.
|
||||||
if (contractWithMultipleAttachments != null) throw TransactionVerificationException.ConflictingAttachmentsRejection(ltx.id, contractWithMultipleAttachments)
|
if (contractWithMultipleAttachments != null) throw ConflictingAttachmentsRejection(ltx.id, contractWithMultipleAttachments)
|
||||||
|
|
||||||
val result = contractAttachmentsPerContract.toMap()
|
val result = contractAttachmentsPerContract.toMap()
|
||||||
|
|
||||||
// Check that there is an attachment for each contract.
|
// Check that there is an attachment for each contract.
|
||||||
if (result.keys != contractClasses) throw TransactionVerificationException.MissingAttachmentRejection(ltx.id, contractClasses.minus(result.keys).first())
|
if (result.keys != contractClasses) throw MissingAttachmentRejection(ltx.id, contractClasses.minus(result.keys).first())
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@ -124,7 +149,7 @@ abstract class Verifier(val ltx: LedgerTransaction, protected val transactionCla
|
|||||||
if (ltx.notary != null && (ltx.inputs.isNotEmpty() || ltx.references.isNotEmpty())) {
|
if (ltx.notary != null && (ltx.inputs.isNotEmpty() || ltx.references.isNotEmpty())) {
|
||||||
ltx.outputs.forEach {
|
ltx.outputs.forEach {
|
||||||
if (it.notary != ltx.notary) {
|
if (it.notary != ltx.notary) {
|
||||||
throw TransactionVerificationException.NotaryChangeInWrongTransactionType(ltx.id, ltx.notary, it.notary)
|
throw NotaryChangeInWrongTransactionType(ltx.id, ltx.notary, it.notary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -156,10 +181,10 @@ abstract class Verifier(val ltx: LedgerTransaction, protected val transactionCla
|
|||||||
it.ref.txhash == ref.txhash && it.ref.index == state.encumbrance
|
it.ref.txhash == ref.txhash && it.ref.index == state.encumbrance
|
||||||
}
|
}
|
||||||
if (!encumbranceStateExists) {
|
if (!encumbranceStateExists) {
|
||||||
throw TransactionVerificationException.TransactionMissingEncumbranceException(
|
throw TransactionMissingEncumbranceException(
|
||||||
ltx.id,
|
ltx.id,
|
||||||
state.encumbrance!!,
|
state.encumbrance!!,
|
||||||
TransactionVerificationException.Direction.INPUT
|
Direction.INPUT
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -194,15 +219,15 @@ abstract class Verifier(val ltx: LedgerTransaction, protected val transactionCla
|
|||||||
statesAndEncumbrance.forEach { (statePosition, encumbrance) ->
|
statesAndEncumbrance.forEach { (statePosition, encumbrance) ->
|
||||||
// Check it does not refer to itself.
|
// Check it does not refer to itself.
|
||||||
if (statePosition == encumbrance || encumbrance >= ltx.outputs.size) {
|
if (statePosition == encumbrance || encumbrance >= ltx.outputs.size) {
|
||||||
throw TransactionVerificationException.TransactionMissingEncumbranceException(
|
throw TransactionMissingEncumbranceException(
|
||||||
ltx.id,
|
ltx.id,
|
||||||
encumbrance,
|
encumbrance,
|
||||||
TransactionVerificationException.Direction.OUTPUT
|
Direction.OUTPUT
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
encumberedSet.add(statePosition) // Guaranteed to have unique elements.
|
encumberedSet.add(statePosition) // Guaranteed to have unique elements.
|
||||||
if (!encumbranceSet.add(encumbrance)) {
|
if (!encumbranceSet.add(encumbrance)) {
|
||||||
throw TransactionVerificationException.TransactionDuplicateEncumbranceException(ltx.id, encumbrance)
|
throw TransactionDuplicateEncumbranceException(ltx.id, encumbrance)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -211,7 +236,7 @@ abstract class Verifier(val ltx: LedgerTransaction, protected val transactionCla
|
|||||||
val symmetricDifference = (encumberedSet union encumbranceSet).subtract(encumberedSet intersect encumbranceSet)
|
val symmetricDifference = (encumberedSet union encumbranceSet).subtract(encumberedSet intersect encumbranceSet)
|
||||||
if (symmetricDifference.isNotEmpty()) {
|
if (symmetricDifference.isNotEmpty()) {
|
||||||
// At least one encumbered state is not in the [encumbranceSet] and vice versa.
|
// At least one encumbered state is not in the [encumbranceSet] and vice versa.
|
||||||
throw TransactionVerificationException.TransactionNonMatchingEncumbranceException(ltx.id, symmetricDifference)
|
throw TransactionNonMatchingEncumbranceException(ltx.id, symmetricDifference)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,7 +260,7 @@ abstract class Verifier(val ltx: LedgerTransaction, protected val transactionCla
|
|||||||
if (indicesAlreadyChecked.add(index)) {
|
if (indicesAlreadyChecked.add(index)) {
|
||||||
val encumbranceIndex = ltx.outputs[index].encumbrance!!
|
val encumbranceIndex = ltx.outputs[index].encumbrance!!
|
||||||
if (ltx.outputs[index].notary != ltx.outputs[encumbranceIndex].notary) {
|
if (ltx.outputs[index].notary != ltx.outputs[encumbranceIndex].notary) {
|
||||||
throw TransactionVerificationException.TransactionNotaryMismatchEncumbranceException(
|
throw TransactionNotaryMismatchEncumbranceException(
|
||||||
ltx.id,
|
ltx.id,
|
||||||
index,
|
index,
|
||||||
encumbranceIndex,
|
encumbranceIndex,
|
||||||
@ -263,7 +288,7 @@ abstract class Verifier(val ltx: LedgerTransaction, protected val transactionCla
|
|||||||
val shouldEnforce = StateContractValidationEnforcementRule.shouldEnforce(state.data)
|
val shouldEnforce = StateContractValidationEnforcementRule.shouldEnforce(state.data)
|
||||||
|
|
||||||
val requiredContractClassName = state.data.requiredContractClassName
|
val requiredContractClassName = state.data.requiredContractClassName
|
||||||
?: if (shouldEnforce) throw TransactionVerificationException.TransactionRequiredContractUnspecifiedException(ltx.id, state) else return
|
?: if (shouldEnforce) throw TransactionRequiredContractUnspecifiedException(ltx.id, state) else return
|
||||||
|
|
||||||
if (state.contract != requiredContractClassName)
|
if (state.contract != requiredContractClassName)
|
||||||
if (shouldEnforce) {
|
if (shouldEnforce) {
|
||||||
@ -310,7 +335,7 @@ abstract class Verifier(val ltx: LedgerTransaction, protected val transactionCla
|
|||||||
outputConstraints.forEach { outputConstraint ->
|
outputConstraints.forEach { outputConstraint ->
|
||||||
inputConstraints.forEach { inputConstraint ->
|
inputConstraints.forEach { inputConstraint ->
|
||||||
if (!(outputConstraint.canBeTransitionedFrom(inputConstraint, contractAttachment))) {
|
if (!(outputConstraint.canBeTransitionedFrom(inputConstraint, contractAttachment))) {
|
||||||
throw TransactionVerificationException.ConstraintPropagationRejection(
|
throw ConstraintPropagationRejection(
|
||||||
ltx.id,
|
ltx.id,
|
||||||
contractClassName,
|
contractClassName,
|
||||||
inputConstraint,
|
inputConstraint,
|
||||||
@ -331,7 +356,7 @@ abstract class Verifier(val ltx: LedgerTransaction, protected val transactionCla
|
|||||||
@Suppress("NestedBlockDepth", "MagicNumber")
|
@Suppress("NestedBlockDepth", "MagicNumber")
|
||||||
private fun verifyConstraints(contractAttachmentsByContract: Map<ContractClassName, ContractAttachment>) {
|
private fun verifyConstraints(contractAttachmentsByContract: Map<ContractClassName, ContractAttachment>) {
|
||||||
// For each contract/constraint pair check that the relevant attachment is valid.
|
// For each contract/constraint pair check that the relevant attachment is valid.
|
||||||
allStates.map { it.contract to it.constraint }.toSet().forEach { (contract, constraint) ->
|
allStates.mapTo(LinkedHashSet()) { it.contract to it.constraint }.forEach { (contract, constraint) ->
|
||||||
if (constraint is SignatureAttachmentConstraint) {
|
if (constraint is SignatureAttachmentConstraint) {
|
||||||
/**
|
/**
|
||||||
* Support for signature constraints has been added on
|
* Support for signature constraints has been added on
|
||||||
@ -346,9 +371,9 @@ abstract class Verifier(val ltx: LedgerTransaction, protected val transactionCla
|
|||||||
"Signature constraints"
|
"Signature constraints"
|
||||||
)
|
)
|
||||||
val constraintKey = constraint.key
|
val constraintKey = constraint.key
|
||||||
if (ltx.networkParameters?.minimumPlatformVersion ?: 1 >= PlatformVersionSwitches.LIMIT_KEYS_IN_SIGNATURE_CONSTRAINTS) {
|
if ((ltx.networkParameters?.minimumPlatformVersion ?: 1) >= PlatformVersionSwitches.LIMIT_KEYS_IN_SIGNATURE_CONSTRAINTS) {
|
||||||
if (constraintKey is CompositeKey && constraintKey.leafKeys.size > MAX_NUMBER_OF_KEYS_IN_SIGNATURE_CONSTRAINT) {
|
if (constraintKey is CompositeKey && constraintKey.leafKeys.size > MAX_NUMBER_OF_KEYS_IN_SIGNATURE_CONSTRAINT) {
|
||||||
throw TransactionVerificationException.InvalidConstraintRejection(ltx.id, contract,
|
throw InvalidConstraintRejection(ltx.id, contract,
|
||||||
"Signature constraint contains composite key with ${constraintKey.leafKeys.size} leaf keys, " +
|
"Signature constraint contains composite key with ${constraintKey.leafKeys.size} leaf keys, " +
|
||||||
"which is more than the maximum allowed number of keys " +
|
"which is more than the maximum allowed number of keys " +
|
||||||
"($MAX_NUMBER_OF_KEYS_IN_SIGNATURE_CONSTRAINT).")
|
"($MAX_NUMBER_OF_KEYS_IN_SIGNATURE_CONSTRAINT).")
|
||||||
@ -364,7 +389,7 @@ abstract class Verifier(val ltx: LedgerTransaction, protected val transactionCla
|
|||||||
if (HashAttachmentConstraint.disableHashConstraints && constraint is HashAttachmentConstraint)
|
if (HashAttachmentConstraint.disableHashConstraints && constraint is HashAttachmentConstraint)
|
||||||
logger.warnOnce("Skipping hash constraints verification.")
|
logger.warnOnce("Skipping hash constraints verification.")
|
||||||
else if (!constraint.isSatisfiedBy(constraintAttachment))
|
else if (!constraint.isSatisfiedBy(constraintAttachment))
|
||||||
throw TransactionVerificationException.ContractConstraintRejection(ltx.id, contract)
|
throw ContractConstraintRejection(ltx.id, contract)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -374,29 +399,12 @@ abstract class Verifier(val ltx: LedgerTransaction, protected val transactionCla
|
|||||||
abstract fun verifyContracts()
|
abstract fun verifyContracts()
|
||||||
}
|
}
|
||||||
|
|
||||||
class BasicVerifier(ltx: LedgerTransaction, transactionClassLoader: ClassLoader) : Verifier(ltx, transactionClassLoader) {
|
|
||||||
/**
|
|
||||||
* Check the transaction is contract-valid by running the verify() for each input and output state contract.
|
|
||||||
* If any contract fails to verify, the whole transaction is considered to be invalid.
|
|
||||||
*
|
|
||||||
* Note: Reference states are not verified.
|
|
||||||
*/
|
|
||||||
override fun verifyContracts() {
|
|
||||||
try {
|
|
||||||
ContractVerifier(transactionClassLoader).apply(ltx)
|
|
||||||
} catch (e: TransactionVerificationException.ContractRejection) {
|
|
||||||
logger.error("Error validating transaction ${ltx.id}.", e.cause)
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify all of the contracts on the given [LedgerTransaction].
|
* Verify all of the contracts on the given [LedgerTransaction].
|
||||||
*/
|
*/
|
||||||
@Suppress("TooGenericExceptionCaught")
|
@Suppress("TooGenericExceptionCaught")
|
||||||
@KeepForDJVM
|
@KeepForDJVM
|
||||||
class ContractVerifier(private val transactionClassLoader: ClassLoader) : Function<LedgerTransaction, Unit> {
|
class ContractVerifier(private val transactionClassLoader: ClassLoader) : Function<Supplier<LedgerTransaction>, Unit> {
|
||||||
// This constructor is used inside the DJVM's sandbox.
|
// This constructor is used inside the DJVM's sandbox.
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
constructor() : this(ClassLoader.getSystemClassLoader())
|
constructor() : this(ClassLoader.getSystemClassLoader())
|
||||||
@ -406,34 +414,45 @@ class ContractVerifier(private val transactionClassLoader: ClassLoader) : Functi
|
|||||||
return try {
|
return try {
|
||||||
Class.forName(contractClassName, false, transactionClassLoader).asSubclass(Contract::class.java)
|
Class.forName(contractClassName, false, transactionClassLoader).asSubclass(Contract::class.java)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw TransactionVerificationException.ContractCreationError(id, contractClassName, e)
|
throw ContractCreationError(id, contractClassName, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun apply(ltx: LedgerTransaction) {
|
private fun generateContracts(ltx: LedgerTransaction): List<Contract> {
|
||||||
val contractClassNames = (ltx.inputs.map(StateAndRef<ContractState>::state) + ltx.outputs)
|
return (ltx.inputs.map(StateAndRef<ContractState>::state) + ltx.outputs)
|
||||||
.mapTo(LinkedHashSet(), TransactionState<*>::contract)
|
.mapTo(LinkedHashSet(), TransactionState<*>::contract)
|
||||||
|
.map { contractClassName ->
|
||||||
contractClassNames.associateBy(
|
createContractClass(ltx.id, contractClassName)
|
||||||
{ it }, { createContractClass(ltx.id, it) }
|
}.map { contractClass ->
|
||||||
).map { (contractClassName, contractClass) ->
|
try {
|
||||||
try {
|
/**
|
||||||
/**
|
* This function must execute within the DJVM's sandbox, which does not
|
||||||
* This function must execute with the DJVM's sandbox, which does not
|
* permit user code to invoke [java.lang.reflect.Constructor.newInstance].
|
||||||
* permit user code to invoke [java.lang.Class.getDeclaredConstructor].
|
* (This would be fixable now, provided the constructor is public.)
|
||||||
*
|
*
|
||||||
* [Class.newInstance] is deprecated as of Java 9.
|
* [Class.newInstance] is deprecated as of Java 9.
|
||||||
*/
|
*/
|
||||||
@Suppress("deprecation")
|
@Suppress("deprecation")
|
||||||
contractClass.newInstance()
|
contractClass.newInstance()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw TransactionVerificationException.ContractCreationError(ltx.id, contractClassName, e)
|
throw ContractCreationError(ltx.id, contractClass.name, e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun apply(transactionFactory: Supplier<LedgerTransaction>) {
|
||||||
|
var firstLtx: LedgerTransaction? = null
|
||||||
|
|
||||||
|
transactionFactory.get().let { ltx ->
|
||||||
|
firstLtx = ltx
|
||||||
|
generateContracts(ltx)
|
||||||
}.forEach { contract ->
|
}.forEach { contract ->
|
||||||
|
val ltx = firstLtx ?: transactionFactory.get()
|
||||||
|
firstLtx = null
|
||||||
try {
|
try {
|
||||||
contract.verify(ltx)
|
contract.verify(ltx)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw TransactionVerificationException.ContractRejection(ltx.id, contract, e)
|
throw ContractRejection(ltx.id, contract, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,8 @@ import net.corda.core.utilities.days
|
|||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
import java.util.Collections.unmodifiableList
|
||||||
|
import java.util.Collections.unmodifiableMap
|
||||||
|
|
||||||
// DOCSTART 1
|
// DOCSTART 1
|
||||||
/**
|
/**
|
||||||
@ -166,6 +168,38 @@ data class NetworkParameters(
|
|||||||
epoch=$epoch
|
epoch=$epoch
|
||||||
}"""
|
}"""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun toImmutable(): NetworkParameters {
|
||||||
|
return NetworkParameters(
|
||||||
|
minimumPlatformVersion = minimumPlatformVersion,
|
||||||
|
notaries = unmodifiable(notaries),
|
||||||
|
maxMessageSize = maxMessageSize,
|
||||||
|
maxTransactionSize = maxTransactionSize,
|
||||||
|
modifiedTime = modifiedTime,
|
||||||
|
epoch = epoch,
|
||||||
|
whitelistedContractImplementations = unmodifiable(whitelistedContractImplementations) { entry ->
|
||||||
|
unmodifiableList(entry.value)
|
||||||
|
},
|
||||||
|
eventHorizon = eventHorizon,
|
||||||
|
packageOwnership = unmodifiable(packageOwnership)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T> unmodifiable(list: List<T>): List<T> {
|
||||||
|
return if (list.isEmpty()) {
|
||||||
|
emptyList()
|
||||||
|
} else {
|
||||||
|
unmodifiableList(list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <K, V> unmodifiable(map: Map<K, V>, transform: (Map.Entry<K, V>) -> V = Map.Entry<K, V>::value): Map<K, V> {
|
||||||
|
return if (map.isEmpty()) {
|
||||||
|
emptyMap()
|
||||||
|
} else {
|
||||||
|
unmodifiableMap(map.mapValues(transform))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -13,6 +13,10 @@ import net.corda.core.utilities.sequence
|
|||||||
import java.io.NotSerializableException
|
import java.io.NotSerializableException
|
||||||
import java.sql.Blob
|
import java.sql.Blob
|
||||||
|
|
||||||
|
const val DESERIALIZATION_CACHE_PROPERTY = "DESERIALIZATION_CACHE"
|
||||||
|
const val AMQP_ENVELOPE_CACHE_PROPERTY = "AMQP_ENVELOPE_CACHE"
|
||||||
|
const val AMQP_ENVELOPE_CACHE_INITIAL_CAPACITY = 256
|
||||||
|
|
||||||
data class ObjectWithCompatibleContext<out T : Any>(val obj: T, val context: SerializationContext)
|
data class ObjectWithCompatibleContext<out T : Any>(val obj: T, val context: SerializationContext)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -65,12 +69,16 @@ abstract class SerializationFactory {
|
|||||||
* Change the current context inside the block to that supplied.
|
* Change the current context inside the block to that supplied.
|
||||||
*/
|
*/
|
||||||
fun <T> withCurrentContext(context: SerializationContext?, block: () -> T): T {
|
fun <T> withCurrentContext(context: SerializationContext?, block: () -> T): T {
|
||||||
val priorContext = _currentContext.get()
|
return if (context == null) {
|
||||||
if (context != null) _currentContext.set(context)
|
block()
|
||||||
try {
|
} else {
|
||||||
return block()
|
val priorContext = _currentContext.get()
|
||||||
} finally {
|
_currentContext.set(context)
|
||||||
if (context != null) _currentContext.set(priorContext)
|
try {
|
||||||
|
block()
|
||||||
|
} finally {
|
||||||
|
_currentContext.set(priorContext)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +20,9 @@ import net.corda.core.internal.createInstancesOfClassesImplementing
|
|||||||
import net.corda.core.internal.createSimpleCache
|
import net.corda.core.internal.createSimpleCache
|
||||||
import net.corda.core.internal.toSynchronised
|
import net.corda.core.internal.toSynchronised
|
||||||
import net.corda.core.node.NetworkParameters
|
import net.corda.core.node.NetworkParameters
|
||||||
|
import net.corda.core.serialization.AMQP_ENVELOPE_CACHE_INITIAL_CAPACITY
|
||||||
|
import net.corda.core.serialization.AMQP_ENVELOPE_CACHE_PROPERTY
|
||||||
|
import net.corda.core.serialization.DESERIALIZATION_CACHE_PROPERTY
|
||||||
import net.corda.core.serialization.SerializationContext
|
import net.corda.core.serialization.SerializationContext
|
||||||
import net.corda.core.serialization.SerializationCustomSerializer
|
import net.corda.core.serialization.SerializationCustomSerializer
|
||||||
import net.corda.core.serialization.SerializationFactory
|
import net.corda.core.serialization.SerializationFactory
|
||||||
@ -39,7 +42,9 @@ import java.net.URLStreamHandler
|
|||||||
import java.net.URLStreamHandlerFactory
|
import java.net.URLStreamHandlerFactory
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.security.Permission
|
import java.security.Permission
|
||||||
import java.util.*
|
import java.util.Locale
|
||||||
|
import java.util.ServiceLoader
|
||||||
|
import java.util.WeakHashMap
|
||||||
import java.util.function.Function
|
import java.util.function.Function
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -67,12 +72,15 @@ class AttachmentsClassLoader(attachments: List<Attachment>,
|
|||||||
init {
|
init {
|
||||||
// Apply our own URLStreamHandlerFactory to resolve attachments
|
// Apply our own URLStreamHandlerFactory to resolve attachments
|
||||||
setOrDecorateURLStreamHandlerFactory()
|
setOrDecorateURLStreamHandlerFactory()
|
||||||
|
|
||||||
|
// Allow AttachmentsClassLoader to be used concurrently.
|
||||||
|
registerAsParallelCapable()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Jolokia and Json-simple are dependencies that were bundled by mistake within contract jars.
|
// Jolokia and Json-simple are dependencies that were bundled by mistake within contract jars.
|
||||||
// In the AttachmentsClassLoader we just block any class in those 2 packages.
|
// In the AttachmentsClassLoader we just block any class in those 2 packages.
|
||||||
private val ignoreDirectories = listOf("org/jolokia/", "org/json/simple/")
|
private val ignoreDirectories = listOf("org/jolokia/", "org/json/simple/")
|
||||||
private val ignorePackages = ignoreDirectories.map { it.replace("/", ".") }
|
private val ignorePackages = ignoreDirectories.map { it.replace('/', '.') }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply our custom factory either directly, if `URL.setURLStreamHandlerFactory` has not been called yet,
|
* Apply our custom factory either directly, if `URL.setURLStreamHandlerFactory` has not been called yet,
|
||||||
@ -176,10 +184,10 @@ class AttachmentsClassLoader(attachments: List<Attachment>,
|
|||||||
// TODO - investigate potential exploits.
|
// TODO - investigate potential exploits.
|
||||||
private fun shouldCheckForNoOverlap(path: String, targetPlatformVersion: Int): Boolean {
|
private fun shouldCheckForNoOverlap(path: String, targetPlatformVersion: Int): Boolean {
|
||||||
require(path.toLowerCase() == path)
|
require(path.toLowerCase() == path)
|
||||||
require(!path.contains("\\"))
|
require(!path.contains('\\'))
|
||||||
|
|
||||||
return when {
|
return when {
|
||||||
path.endsWith("/") -> false // Directories (packages) can overlap.
|
path.endsWith('/') -> false // Directories (packages) can overlap.
|
||||||
targetPlatformVersion < PlatformVersionSwitches.IGNORE_JOLOKIA_JSON_SIMPLE_IN_CORDAPPS &&
|
targetPlatformVersion < PlatformVersionSwitches.IGNORE_JOLOKIA_JSON_SIMPLE_IN_CORDAPPS &&
|
||||||
ignoreDirectories.any { path.startsWith(it) } -> false // Ignore jolokia and json-simple for old cordapps.
|
ignoreDirectories.any { path.startsWith(it) } -> false // Ignore jolokia and json-simple for old cordapps.
|
||||||
path.endsWith(".class") -> true // All class files need to be unique.
|
path.endsWith(".class") -> true // All class files need to be unique.
|
||||||
@ -219,7 +227,7 @@ class AttachmentsClassLoader(attachments: List<Attachment>,
|
|||||||
// attacks on externally connected systems that only consider type names, we allow people to formally
|
// attacks on externally connected systems that only consider type names, we allow people to formally
|
||||||
// claim their parts of the Java package namespace via registration with the zone operator.
|
// claim their parts of the Java package namespace via registration with the zone operator.
|
||||||
|
|
||||||
val classLoaderEntries = mutableMapOf<String, SecureHash.SHA256>()
|
val classLoaderEntries = mutableMapOf<String, SecureHash>()
|
||||||
val ctx = AttachmentHashContext(sampleTxId)
|
val ctx = AttachmentHashContext(sampleTxId)
|
||||||
for (attachment in attachments) {
|
for (attachment in attachments) {
|
||||||
// We may have been given an attachment loaded from the database in which case, important info like
|
// We may have been given an attachment loaded from the database in which case, important info like
|
||||||
@ -238,7 +246,7 @@ class AttachmentsClassLoader(attachments: List<Attachment>,
|
|||||||
// signed by the owners of the packages, even if it's not. We'd eventually discover that fact
|
// signed by the owners of the packages, even if it's not. We'd eventually discover that fact
|
||||||
// when trying to read the class file to use it, but if we'd made any decisions based on
|
// when trying to read the class file to use it, but if we'd made any decisions based on
|
||||||
// perceived correctness of the signatures or package ownership already, that would be too late.
|
// perceived correctness of the signatures or package ownership already, that would be too late.
|
||||||
attachment.openAsJAR().use { JarSignatureCollector.collectSigners(it) }
|
attachment.openAsJAR().use(JarSignatureCollector::collectSigners)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now open it again to compute the overlap and package ownership data.
|
// Now open it again to compute the overlap and package ownership data.
|
||||||
@ -309,11 +317,11 @@ class AttachmentsClassLoader(attachments: List<Attachment>,
|
|||||||
* Required to prevent classes that were excluded from the no-overlap check from being loaded by contract code.
|
* Required to prevent classes that were excluded from the no-overlap check from being loaded by contract code.
|
||||||
* As it can lead to non-determinism.
|
* As it can lead to non-determinism.
|
||||||
*/
|
*/
|
||||||
override fun loadClass(name: String?): Class<*> {
|
override fun loadClass(name: String, resolve: Boolean): Class<*>? {
|
||||||
if (ignorePackages.any { name!!.startsWith(it) }) {
|
if (ignorePackages.any { name.startsWith(it) }) {
|
||||||
throw ClassNotFoundException(name)
|
throw ClassNotFoundException(name)
|
||||||
}
|
}
|
||||||
return super.loadClass(name)
|
return super.loadClass(name, resolve)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -323,7 +331,7 @@ class AttachmentsClassLoader(attachments: List<Attachment>,
|
|||||||
*/
|
*/
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
object AttachmentsClassLoaderBuilder {
|
object AttachmentsClassLoaderBuilder {
|
||||||
const val CACHE_SIZE = 16
|
private const val CACHE_SIZE = 16
|
||||||
|
|
||||||
private val fallBackCache: AttachmentsClassLoaderCache = AttachmentsClassLoaderSimpleCacheImpl(CACHE_SIZE)
|
private val fallBackCache: AttachmentsClassLoaderCache = AttachmentsClassLoaderSimpleCacheImpl(CACHE_SIZE)
|
||||||
|
|
||||||
@ -339,13 +347,13 @@ object AttachmentsClassLoaderBuilder {
|
|||||||
isAttachmentTrusted: (Attachment) -> Boolean,
|
isAttachmentTrusted: (Attachment) -> Boolean,
|
||||||
parent: ClassLoader = ClassLoader.getSystemClassLoader(),
|
parent: ClassLoader = ClassLoader.getSystemClassLoader(),
|
||||||
attachmentsClassLoaderCache: AttachmentsClassLoaderCache?,
|
attachmentsClassLoaderCache: AttachmentsClassLoaderCache?,
|
||||||
block: (ClassLoader) -> T): T {
|
block: (SerializationContext) -> T): T {
|
||||||
val attachmentIds = attachments.map(Attachment::id).toSet()
|
val attachmentIds = attachments.mapTo(LinkedHashSet(), Attachment::id)
|
||||||
|
|
||||||
val cache = attachmentsClassLoaderCache ?: fallBackCache
|
val cache = attachmentsClassLoaderCache ?: fallBackCache
|
||||||
val serializationContext = cache.computeIfAbsent(AttachmentsClassLoaderKey(attachmentIds, params), Function {
|
val serializationContext = cache.computeIfAbsent(AttachmentsClassLoaderKey(attachmentIds, params), Function { key ->
|
||||||
// Create classloader and load serializers, whitelisted classes
|
// Create classloader and load serializers, whitelisted classes
|
||||||
val transactionClassLoader = AttachmentsClassLoader(attachments, params, txId, isAttachmentTrusted, parent)
|
val transactionClassLoader = AttachmentsClassLoader(attachments, key.params, txId, isAttachmentTrusted, parent)
|
||||||
val serializers = try {
|
val serializers = try {
|
||||||
createInstancesOfClassesImplementing(transactionClassLoader, SerializationCustomSerializer::class.java,
|
createInstancesOfClassesImplementing(transactionClassLoader, SerializationCustomSerializer::class.java,
|
||||||
JDK1_2_CLASS_FILE_FORMAT_MAJOR_VERSION..JDK8_CLASS_FILE_FORMAT_MAJOR_VERSION)
|
JDK1_2_CLASS_FILE_FORMAT_MAJOR_VERSION..JDK8_CLASS_FILE_FORMAT_MAJOR_VERSION)
|
||||||
@ -366,11 +374,16 @@ object AttachmentsClassLoaderBuilder {
|
|||||||
.withWhitelist(whitelistedClasses)
|
.withWhitelist(whitelistedClasses)
|
||||||
.withCustomSerializers(serializers)
|
.withCustomSerializers(serializers)
|
||||||
.withoutCarpenter()
|
.withoutCarpenter()
|
||||||
})
|
}).withProperties(mapOf<Any, Any>(
|
||||||
|
// Duplicate the SerializationContext from the cache and give
|
||||||
|
// it these extra properties, just for this transaction.
|
||||||
|
AMQP_ENVELOPE_CACHE_PROPERTY to HashMap<Any, Any>(AMQP_ENVELOPE_CACHE_INITIAL_CAPACITY),
|
||||||
|
DESERIALIZATION_CACHE_PROPERTY to HashMap<Any, Any>()
|
||||||
|
))
|
||||||
|
|
||||||
// Deserialize all relevant classes in the transaction classloader.
|
// Deserialize all relevant classes in the transaction classloader.
|
||||||
return SerializationFactory.defaultFactory.withCurrentContext(serializationContext) {
|
return SerializationFactory.defaultFactory.withCurrentContext(serializationContext) {
|
||||||
block(serializationContext.deserializationClassLoader)
|
block(serializationContext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -145,7 +145,7 @@ data class ContractUpgradeWireTransaction(
|
|||||||
|
|
||||||
private fun upgradedContract(className: ContractClassName, classLoader: ClassLoader): UpgradedContract<ContractState, ContractState> = try {
|
private fun upgradedContract(className: ContractClassName, classLoader: ClassLoader): UpgradedContract<ContractState, ContractState> = try {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
classLoader.loadClass(className).asSubclass(UpgradedContract::class.java).getDeclaredConstructor().newInstance() as UpgradedContract<ContractState, ContractState>
|
Class.forName(className, false, classLoader).asSubclass(UpgradedContract::class.java).getDeclaredConstructor().newInstance() as UpgradedContract<ContractState, ContractState>
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw TransactionVerificationException.ContractCreationError(id, className, e)
|
throw TransactionVerificationException.ContractCreationError(id, className, e)
|
||||||
}
|
}
|
||||||
@ -166,9 +166,9 @@ data class ContractUpgradeWireTransaction(
|
|||||||
params,
|
params,
|
||||||
id,
|
id,
|
||||||
{ (services as ServiceHubCoreInternal).attachmentTrustCalculator.calculate(it) },
|
{ (services as ServiceHubCoreInternal).attachmentTrustCalculator.calculate(it) },
|
||||||
attachmentsClassLoaderCache = (services as ServiceHubCoreInternal).attachmentsClassLoaderCache) { transactionClassLoader ->
|
attachmentsClassLoaderCache = (services as ServiceHubCoreInternal).attachmentsClassLoaderCache) { serializationContext ->
|
||||||
val resolvedInput = binaryInput.deserialize()
|
val resolvedInput = binaryInput.deserialize()
|
||||||
val upgradedContract = upgradedContract(upgradedContractClassName, transactionClassLoader)
|
val upgradedContract = upgradedContract(upgradedContractClassName, serializationContext.deserializationClassLoader)
|
||||||
val outputState = calculateUpgradedState(resolvedInput, upgradedContract, upgradedAttachment)
|
val outputState = calculateUpgradedState(resolvedInput, upgradedContract, upgradedAttachment)
|
||||||
outputState.serialize()
|
outputState.serialize()
|
||||||
}
|
}
|
||||||
@ -311,8 +311,7 @@ private constructor(
|
|||||||
@CordaInternal
|
@CordaInternal
|
||||||
internal fun loadUpgradedContract(upgradedContractClassName: ContractClassName, classLoader: ClassLoader): UpgradedContract<ContractState, *> {
|
internal fun loadUpgradedContract(upgradedContractClassName: ContractClassName, classLoader: ClassLoader): UpgradedContract<ContractState, *> {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
return classLoader
|
return Class.forName(upgradedContractClassName, false, classLoader)
|
||||||
.loadClass(upgradedContractClassName)
|
|
||||||
.asSubclass(Contract::class.java)
|
.asSubclass(Contract::class.java)
|
||||||
.getConstructor()
|
.getConstructor()
|
||||||
.newInstance() as UpgradedContract<ContractState, *>
|
.newInstance() as UpgradedContract<ContractState, *>
|
||||||
|
@ -18,21 +18,25 @@ import net.corda.core.crypto.DigestService
|
|||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.internal.BasicVerifier
|
import net.corda.core.internal.ContractVerifier
|
||||||
import net.corda.core.internal.SerializedStateAndRef
|
import net.corda.core.internal.SerializedStateAndRef
|
||||||
import net.corda.core.internal.Verifier
|
import net.corda.core.internal.Verifier
|
||||||
import net.corda.core.internal.castIfPossible
|
import net.corda.core.internal.castIfPossible
|
||||||
import net.corda.core.internal.deserialiseCommands
|
import net.corda.core.internal.deserialiseCommands
|
||||||
import net.corda.core.internal.deserialiseComponentGroup
|
import net.corda.core.internal.deserialiseComponentGroup
|
||||||
|
import net.corda.core.internal.eagerDeserialise
|
||||||
import net.corda.core.internal.isUploaderTrusted
|
import net.corda.core.internal.isUploaderTrusted
|
||||||
import net.corda.core.internal.uncheckedCast
|
import net.corda.core.internal.uncheckedCast
|
||||||
import net.corda.core.node.NetworkParameters
|
import net.corda.core.node.NetworkParameters
|
||||||
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
|
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
|
||||||
|
import net.corda.core.serialization.SerializationContext
|
||||||
|
import net.corda.core.serialization.SerializationFactory
|
||||||
import net.corda.core.serialization.internal.AttachmentsClassLoaderCache
|
import net.corda.core.serialization.internal.AttachmentsClassLoaderCache
|
||||||
import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder
|
import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder
|
||||||
import net.corda.core.utilities.contextLogger
|
import net.corda.core.utilities.contextLogger
|
||||||
import java.util.Collections.unmodifiableList
|
import java.util.Collections.unmodifiableList
|
||||||
import java.util.function.Predicate
|
import java.util.function.Predicate
|
||||||
|
import java.util.function.Supplier
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A LedgerTransaction is derived from a [WireTransaction]. It is the result of doing the following operations:
|
* A LedgerTransaction is derived from a [WireTransaction]. It is the result of doing the following operations:
|
||||||
@ -90,7 +94,7 @@ private constructor(
|
|||||||
private val serializedInputs: List<SerializedStateAndRef>?,
|
private val serializedInputs: List<SerializedStateAndRef>?,
|
||||||
private val serializedReferences: List<SerializedStateAndRef>?,
|
private val serializedReferences: List<SerializedStateAndRef>?,
|
||||||
private val isAttachmentTrusted: (Attachment) -> Boolean,
|
private val isAttachmentTrusted: (Attachment) -> Boolean,
|
||||||
private val verifierFactory: (LedgerTransaction, ClassLoader) -> Verifier,
|
private val verifierFactory: (LedgerTransaction, SerializationContext) -> Verifier,
|
||||||
private val attachmentsClassLoaderCache: AttachmentsClassLoaderCache?,
|
private val attachmentsClassLoaderCache: AttachmentsClassLoaderCache?,
|
||||||
val digestService: DigestService
|
val digestService: DigestService
|
||||||
) : FullTransaction() {
|
) : FullTransaction() {
|
||||||
@ -100,22 +104,23 @@ private constructor(
|
|||||||
*/
|
*/
|
||||||
@DeprecatedConstructorForDeserialization(1)
|
@DeprecatedConstructorForDeserialization(1)
|
||||||
private constructor(
|
private constructor(
|
||||||
inputs: List<StateAndRef<ContractState>>,
|
inputs: List<StateAndRef<ContractState>>,
|
||||||
outputs: List<TransactionState<ContractState>>,
|
outputs: List<TransactionState<ContractState>>,
|
||||||
commands: List<CommandWithParties<CommandData>>,
|
commands: List<CommandWithParties<CommandData>>,
|
||||||
attachments: List<Attachment>,
|
attachments: List<Attachment>,
|
||||||
id: SecureHash,
|
id: SecureHash,
|
||||||
notary: Party?,
|
notary: Party?,
|
||||||
timeWindow: TimeWindow?,
|
timeWindow: TimeWindow?,
|
||||||
privacySalt: PrivacySalt,
|
privacySalt: PrivacySalt,
|
||||||
networkParameters: NetworkParameters?,
|
networkParameters: NetworkParameters?,
|
||||||
references: List<StateAndRef<ContractState>>,
|
references: List<StateAndRef<ContractState>>,
|
||||||
componentGroups: List<ComponentGroup>?,
|
componentGroups: List<ComponentGroup>?,
|
||||||
serializedInputs: List<SerializedStateAndRef>?,
|
serializedInputs: List<SerializedStateAndRef>?,
|
||||||
serializedReferences: List<SerializedStateAndRef>?,
|
serializedReferences: List<SerializedStateAndRef>?,
|
||||||
isAttachmentTrusted: (Attachment) -> Boolean,
|
isAttachmentTrusted: (Attachment) -> Boolean,
|
||||||
verifierFactory: (LedgerTransaction, ClassLoader) -> Verifier,
|
verifierFactory: (LedgerTransaction, SerializationContext) -> Verifier,
|
||||||
attachmentsClassLoaderCache: AttachmentsClassLoaderCache?) : this(
|
attachmentsClassLoaderCache: AttachmentsClassLoaderCache?
|
||||||
|
) : this(
|
||||||
inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt,
|
inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt,
|
||||||
networkParameters, references, componentGroups, serializedInputs, serializedReferences,
|
networkParameters, references, componentGroups, serializedInputs, serializedReferences,
|
||||||
isAttachmentTrusted, verifierFactory, attachmentsClassLoaderCache, DigestService.sha2_256)
|
isAttachmentTrusted, verifierFactory, attachmentsClassLoaderCache, DigestService.sha2_256)
|
||||||
@ -124,8 +129,8 @@ private constructor(
|
|||||||
companion object {
|
companion object {
|
||||||
private val logger = contextLogger()
|
private val logger = contextLogger()
|
||||||
|
|
||||||
private fun <T> protect(list: List<T>?): List<T>? {
|
private fun <T> protect(list: List<T>): List<T> {
|
||||||
return list?.run {
|
return list.run {
|
||||||
if (isEmpty()) {
|
if (isEmpty()) {
|
||||||
emptyList()
|
emptyList()
|
||||||
} else {
|
} else {
|
||||||
@ -134,6 +139,8 @@ private constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun <T> protectOrNull(list: List<T>?): List<T>? = list?.let(::protect)
|
||||||
|
|
||||||
@CordaInternal
|
@CordaInternal
|
||||||
internal fun create(
|
internal fun create(
|
||||||
inputs: List<StateAndRef<ContractState>>,
|
inputs: List<StateAndRef<ContractState>>,
|
||||||
@ -164,9 +171,9 @@ private constructor(
|
|||||||
privacySalt = privacySalt,
|
privacySalt = privacySalt,
|
||||||
networkParameters = networkParameters,
|
networkParameters = networkParameters,
|
||||||
references = references,
|
references = references,
|
||||||
componentGroups = protect(componentGroups),
|
componentGroups = protectOrNull(componentGroups),
|
||||||
serializedInputs = protect(serializedInputs),
|
serializedInputs = protectOrNull(serializedInputs),
|
||||||
serializedReferences = protect(serializedReferences),
|
serializedReferences = protectOrNull(serializedReferences),
|
||||||
isAttachmentTrusted = isAttachmentTrusted,
|
isAttachmentTrusted = isAttachmentTrusted,
|
||||||
verifierFactory = ::BasicVerifier,
|
verifierFactory = ::BasicVerifier,
|
||||||
attachmentsClassLoaderCache = attachmentsClassLoaderCache,
|
attachmentsClassLoaderCache = attachmentsClassLoaderCache,
|
||||||
@ -176,10 +183,11 @@ private constructor(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This factory function will create an instance of [LedgerTransaction]
|
* This factory function will create an instance of [LedgerTransaction]
|
||||||
* that will be used inside the DJVM sandbox.
|
* that will be used for contract verification. See [BasicVerifier] and
|
||||||
|
* [DeterministicVerifier][net.corda.node.internal.djvm.DeterministicVerifier].
|
||||||
*/
|
*/
|
||||||
@CordaInternal
|
@CordaInternal
|
||||||
fun createForSandbox(
|
fun createForContractVerify(
|
||||||
inputs: List<StateAndRef<ContractState>>,
|
inputs: List<StateAndRef<ContractState>>,
|
||||||
outputs: List<TransactionState<ContractState>>,
|
outputs: List<TransactionState<ContractState>>,
|
||||||
commands: List<CommandWithParties<CommandData>>,
|
commands: List<CommandWithParties<CommandData>>,
|
||||||
@ -188,28 +196,31 @@ private constructor(
|
|||||||
notary: Party?,
|
notary: Party?,
|
||||||
timeWindow: TimeWindow?,
|
timeWindow: TimeWindow?,
|
||||||
privacySalt: PrivacySalt,
|
privacySalt: PrivacySalt,
|
||||||
networkParameters: NetworkParameters,
|
networkParameters: NetworkParameters?,
|
||||||
references: List<StateAndRef<ContractState>>,
|
references: List<StateAndRef<ContractState>>,
|
||||||
digestService: DigestService): LedgerTransaction {
|
digestService: DigestService): LedgerTransaction {
|
||||||
return LedgerTransaction(
|
return LedgerTransaction(
|
||||||
inputs = inputs,
|
inputs = protect(inputs),
|
||||||
outputs = outputs,
|
outputs = protect(outputs),
|
||||||
commands = commands,
|
commands = protect(commands),
|
||||||
attachments = attachments,
|
attachments = protect(attachments),
|
||||||
id = id,
|
id = id,
|
||||||
notary = notary,
|
notary = notary,
|
||||||
timeWindow = timeWindow,
|
timeWindow = timeWindow,
|
||||||
privacySalt = privacySalt,
|
privacySalt = privacySalt,
|
||||||
networkParameters = networkParameters,
|
networkParameters = networkParameters,
|
||||||
references = references,
|
references = protect(references),
|
||||||
componentGroups = null,
|
componentGroups = null,
|
||||||
serializedInputs = null,
|
serializedInputs = null,
|
||||||
serializedReferences = null,
|
serializedReferences = null,
|
||||||
isAttachmentTrusted = { true },
|
isAttachmentTrusted = { true },
|
||||||
verifierFactory = ::BasicVerifier,
|
verifierFactory = ::NoOpVerifier,
|
||||||
attachmentsClassLoaderCache = null,
|
attachmentsClassLoaderCache = null,
|
||||||
digestService = digestService
|
digestService = digestService
|
||||||
)
|
// This check accesses input states and must run on the LedgerTransaction
|
||||||
|
// instance that is verified, not on the outer LedgerTransaction shell.
|
||||||
|
// All states must also deserialize using the correct SerializationContext.
|
||||||
|
).also(LedgerTransaction::checkBaseInvariants)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,11 +262,17 @@ private constructor(
|
|||||||
getParamsWithGoo(),
|
getParamsWithGoo(),
|
||||||
id,
|
id,
|
||||||
isAttachmentTrusted = isAttachmentTrusted,
|
isAttachmentTrusted = isAttachmentTrusted,
|
||||||
attachmentsClassLoaderCache = attachmentsClassLoaderCache) { transactionClassLoader ->
|
attachmentsClassLoaderCache = attachmentsClassLoaderCache) { serializationContext ->
|
||||||
// Create a copy of the outer LedgerTransaction which deserializes all fields inside the [transactionClassLoader].
|
|
||||||
|
// Legacy check - warns if the LedgerTransaction was created incorrectly.
|
||||||
|
checkLtxForVerification()
|
||||||
|
|
||||||
|
// Create a copy of the outer LedgerTransaction which deserializes all fields using
|
||||||
|
// the serialization context (or its deserializationClassloader).
|
||||||
// Only the copy will be used for verification, and the outer shell will be discarded.
|
// Only the copy will be used for verification, and the outer shell will be discarded.
|
||||||
// This artifice is required to preserve backwards compatibility.
|
// This artifice is required to preserve backwards compatibility.
|
||||||
verifierFactory(createLtxForVerification(), transactionClassLoader)
|
// NOTE: The Verifier creates the copies of the LedgerTransaction object now.
|
||||||
|
verifierFactory(this, serializationContext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -272,7 +289,7 @@ private constructor(
|
|||||||
* Node without changing either the wire format or any public APIs.
|
* Node without changing either the wire format or any public APIs.
|
||||||
*/
|
*/
|
||||||
@CordaInternal
|
@CordaInternal
|
||||||
fun specialise(alternateVerifier: (LedgerTransaction, ClassLoader) -> Verifier): LedgerTransaction = LedgerTransaction(
|
fun specialise(alternateVerifier: (LedgerTransaction, SerializationContext) -> Verifier): LedgerTransaction = LedgerTransaction(
|
||||||
inputs = inputs,
|
inputs = inputs,
|
||||||
outputs = outputs,
|
outputs = outputs,
|
||||||
commands = commands,
|
commands = commands,
|
||||||
@ -287,7 +304,11 @@ private constructor(
|
|||||||
serializedInputs = serializedInputs,
|
serializedInputs = serializedInputs,
|
||||||
serializedReferences = serializedReferences,
|
serializedReferences = serializedReferences,
|
||||||
isAttachmentTrusted = isAttachmentTrusted,
|
isAttachmentTrusted = isAttachmentTrusted,
|
||||||
verifierFactory = alternateVerifier,
|
verifierFactory = if (verifierFactory == ::NoOpVerifier) {
|
||||||
|
throw IllegalStateException("Cannot specialise transaction while verifying contracts")
|
||||||
|
} else {
|
||||||
|
alternateVerifier
|
||||||
|
},
|
||||||
attachmentsClassLoaderCache = attachmentsClassLoaderCache,
|
attachmentsClassLoaderCache = attachmentsClassLoaderCache,
|
||||||
digestService = digestService
|
digestService = digestService
|
||||||
)
|
)
|
||||||
@ -319,58 +340,12 @@ private constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the [LedgerTransaction] instance that will be used by contract verification.
|
|
||||||
*
|
|
||||||
* This method needs to run in the special transaction attachments classloader context.
|
|
||||||
*/
|
*/
|
||||||
private fun createLtxForVerification(): LedgerTransaction {
|
private fun checkLtxForVerification() {
|
||||||
val serializedInputs = this.serializedInputs
|
if (serializedInputs == null || serializedReferences == null || componentGroups == null) {
|
||||||
val serializedReferences = this.serializedReferences
|
|
||||||
val componentGroups = this.componentGroups
|
|
||||||
|
|
||||||
val transaction= if (serializedInputs != null && serializedReferences != null && componentGroups != null) {
|
|
||||||
// Deserialize all relevant classes in the transaction classloader.
|
|
||||||
val deserializedInputs = serializedInputs.map { it.toStateAndRef() }
|
|
||||||
val deserializedReferences = serializedReferences.map { it.toStateAndRef() }
|
|
||||||
val deserializedOutputs = deserialiseComponentGroup(componentGroups, TransactionState::class, ComponentGroupEnum.OUTPUTS_GROUP, forceDeserialize = true)
|
|
||||||
val deserializedCommands = deserialiseCommands(componentGroups, forceDeserialize = true, digestService = digestService)
|
|
||||||
val authenticatedDeserializedCommands = deserializedCommands.map { cmd ->
|
|
||||||
@Suppress("DEPRECATION") // Deprecated feature.
|
|
||||||
val parties = commands.find { it.value.javaClass.name == cmd.value.javaClass.name }!!.signingParties
|
|
||||||
CommandWithParties(cmd.signers, parties, cmd.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
LedgerTransaction(
|
|
||||||
inputs = deserializedInputs,
|
|
||||||
outputs = deserializedOutputs,
|
|
||||||
commands = authenticatedDeserializedCommands,
|
|
||||||
attachments = this.attachments,
|
|
||||||
id = this.id,
|
|
||||||
notary = this.notary,
|
|
||||||
timeWindow = this.timeWindow,
|
|
||||||
privacySalt = this.privacySalt,
|
|
||||||
networkParameters = this.networkParameters,
|
|
||||||
references = deserializedReferences,
|
|
||||||
componentGroups = componentGroups,
|
|
||||||
serializedInputs = serializedInputs,
|
|
||||||
serializedReferences = serializedReferences,
|
|
||||||
isAttachmentTrusted = isAttachmentTrusted,
|
|
||||||
verifierFactory = verifierFactory,
|
|
||||||
attachmentsClassLoaderCache = attachmentsClassLoaderCache,
|
|
||||||
digestService = digestService
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// This branch is only present for backwards compatibility.
|
|
||||||
logger.warn("The LedgerTransaction should not be instantiated directly from client code. Please use WireTransaction.toLedgerTransaction." +
|
logger.warn("The LedgerTransaction should not be instantiated directly from client code. Please use WireTransaction.toLedgerTransaction." +
|
||||||
"The result of the verify method might not be accurate.")
|
"The result of the verify method might not be accurate.")
|
||||||
this
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This check accesses input states and must be run in this context.
|
|
||||||
// It must run on the instance that is verified, not on the outer LedgerTransaction shell.
|
|
||||||
transaction.checkBaseInvariants()
|
|
||||||
|
|
||||||
return transaction
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -740,7 +715,7 @@ private constructor(
|
|||||||
componentGroups = null,
|
componentGroups = null,
|
||||||
serializedInputs = null,
|
serializedInputs = null,
|
||||||
serializedReferences = null,
|
serializedReferences = null,
|
||||||
isAttachmentTrusted = { it.isUploaderTrusted() },
|
isAttachmentTrusted = Attachment::isUploaderTrusted,
|
||||||
verifierFactory = ::BasicVerifier,
|
verifierFactory = ::BasicVerifier,
|
||||||
attachmentsClassLoaderCache = null
|
attachmentsClassLoaderCache = null
|
||||||
)
|
)
|
||||||
@ -770,7 +745,7 @@ private constructor(
|
|||||||
componentGroups = null,
|
componentGroups = null,
|
||||||
serializedInputs = null,
|
serializedInputs = null,
|
||||||
serializedReferences = null,
|
serializedReferences = null,
|
||||||
isAttachmentTrusted = { it.isUploaderTrusted() },
|
isAttachmentTrusted = Attachment::isUploaderTrusted,
|
||||||
verifierFactory = ::BasicVerifier,
|
verifierFactory = ::BasicVerifier,
|
||||||
attachmentsClassLoaderCache = null
|
attachmentsClassLoaderCache = null
|
||||||
)
|
)
|
||||||
@ -838,3 +813,89 @@ private constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the default [Verifier] that configures Corda
|
||||||
|
* to execute [Contract.verify(LedgerTransaction)].
|
||||||
|
*
|
||||||
|
* THIS CLASS IS NOT PUBLIC API, AND IS DELIBERATELY PRIVATE!
|
||||||
|
*/
|
||||||
|
@CordaInternal
|
||||||
|
private class BasicVerifier(
|
||||||
|
ltx: LedgerTransaction,
|
||||||
|
private val serializationContext: SerializationContext
|
||||||
|
) : Verifier(ltx, serializationContext.deserializationClassLoader) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
// This is a sanity check: We should only instantiate this
|
||||||
|
// class from [LedgerTransaction.internalPrepareVerify].
|
||||||
|
require(serializationContext === SerializationFactory.defaultFactory.currentContext) {
|
||||||
|
"BasicVerifier for TX ${ltx.id} created outside its SerializationContext"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch these commands' signing parties from the database.
|
||||||
|
// Corda forbids database access during contract verification,
|
||||||
|
// and so we must load the commands here eagerly instead.
|
||||||
|
ltx.commands.eagerDeserialise()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createTransaction(): LedgerTransaction {
|
||||||
|
// Deserialize all relevant classes using the serializationContext.
|
||||||
|
return SerializationFactory.defaultFactory.withCurrentContext(serializationContext) {
|
||||||
|
ltx.transform { componentGroups, serializedInputs, serializedReferences ->
|
||||||
|
val deserializedInputs = serializedInputs.map(SerializedStateAndRef::toStateAndRef)
|
||||||
|
val deserializedReferences = serializedReferences.map(SerializedStateAndRef::toStateAndRef)
|
||||||
|
val deserializedOutputs = deserialiseComponentGroup(componentGroups, TransactionState::class, ComponentGroupEnum.OUTPUTS_GROUP, forceDeserialize = true)
|
||||||
|
val deserializedCommands = deserialiseCommands(componentGroups, forceDeserialize = true, digestService = ltx.digestService)
|
||||||
|
val authenticatedDeserializedCommands = deserializedCommands.mapIndexed { idx, cmd ->
|
||||||
|
// Requires ltx.commands to have been deserialized already.
|
||||||
|
@Suppress("DEPRECATION") // Deprecated feature.
|
||||||
|
val parties = ltx.commands[idx].signingParties
|
||||||
|
CommandWithParties(cmd.signers, parties, cmd.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
LedgerTransaction.createForContractVerify(
|
||||||
|
inputs = deserializedInputs,
|
||||||
|
outputs = deserializedOutputs,
|
||||||
|
commands = authenticatedDeserializedCommands,
|
||||||
|
attachments = ltx.attachments,
|
||||||
|
id = ltx.id,
|
||||||
|
notary = ltx.notary,
|
||||||
|
timeWindow = ltx.timeWindow,
|
||||||
|
privacySalt = ltx.privacySalt,
|
||||||
|
networkParameters = ltx.networkParameters,
|
||||||
|
references = deserializedReferences,
|
||||||
|
digestService = ltx.digestService
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check the transaction is contract-valid by running verify() for each input and output state contract.
|
||||||
|
* If any contract fails to verify, the whole transaction is considered to be invalid.
|
||||||
|
*
|
||||||
|
* Note: Reference states are not verified.
|
||||||
|
*/
|
||||||
|
override fun verifyContracts() {
|
||||||
|
try {
|
||||||
|
ContractVerifier(transactionClassLoader).apply(Supplier(::createTransaction))
|
||||||
|
} catch (e: TransactionVerificationException) {
|
||||||
|
logger.error("Error validating transaction ${ltx.id}.", e.cause)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A "do nothing" [Verifier] installed for contract verification.
|
||||||
|
*
|
||||||
|
* THIS CLASS IS NOT PUBLIC API, AND IS DELIBERATELY PRIVATE!
|
||||||
|
*/
|
||||||
|
@CordaInternal
|
||||||
|
private class NoOpVerifier(ltx: LedgerTransaction, serializationContext: SerializationContext)
|
||||||
|
: Verifier(ltx, serializationContext.deserializationClassLoader) {
|
||||||
|
// Invoking LedgerTransaction.verify() from Contract.verify(LedgerTransaction)
|
||||||
|
// will execute this function. But why would anyone do that?!
|
||||||
|
override fun verifyContracts() {}
|
||||||
|
}
|
||||||
|
@ -154,7 +154,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
|||||||
resolveAttachment,
|
resolveAttachment,
|
||||||
{ stateRef -> resolveStateRef(stateRef)?.serialize() },
|
{ stateRef -> resolveStateRef(stateRef)?.serialize() },
|
||||||
{ null },
|
{ null },
|
||||||
{ it.isUploaderTrusted() },
|
Attachment::isUploaderTrusted,
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -214,7 +214,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
|||||||
notary,
|
notary,
|
||||||
timeWindow,
|
timeWindow,
|
||||||
privacySalt,
|
privacySalt,
|
||||||
resolvedNetworkParameters,
|
resolvedNetworkParameters.toImmutable(),
|
||||||
resolvedReferences,
|
resolvedReferences,
|
||||||
componentGroups,
|
componentGroups,
|
||||||
serializedResolvedInputs,
|
serializedResolvedInputs,
|
||||||
|
@ -90,7 +90,7 @@ object KryoCheckpointSerializer : CheckpointSerializer {
|
|||||||
*/
|
*/
|
||||||
private fun getInputClassForCustomSerializer(classLoader: ClassLoader, customSerializer: CustomSerializerCheckpointAdaptor<*, *>): Class<*> {
|
private fun getInputClassForCustomSerializer(classLoader: ClassLoader, customSerializer: CustomSerializerCheckpointAdaptor<*, *>): Class<*> {
|
||||||
val typeNameWithoutGenerics = customSerializer.cordappType.typeName.substringBefore('<')
|
val typeNameWithoutGenerics = customSerializer.cordappType.typeName.substringBefore('<')
|
||||||
return classLoader.loadClass(typeNameWithoutGenerics)
|
return Class.forName(typeNameWithoutGenerics, false, classLoader)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -5,35 +5,43 @@ import net.corda.core.contracts.CommandWithParties
|
|||||||
import net.corda.core.internal.lazyMapped
|
import net.corda.core.internal.lazyMapped
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.util.function.Function
|
import java.util.function.Function
|
||||||
|
import java.util.function.Supplier
|
||||||
|
|
||||||
class CommandBuilder : Function<Array<Any?>, List<CommandWithParties<CommandData>>> {
|
class CommandBuilder : Function<Array<Any?>, Supplier<List<CommandWithParties<CommandData>>>> {
|
||||||
@Suppress("unchecked_cast")
|
@Suppress("unchecked_cast")
|
||||||
override fun apply(inputs: Array<Any?>): List<CommandWithParties<CommandData>> {
|
override fun apply(inputs: Array<Any?>): Supplier<List<CommandWithParties<CommandData>>> {
|
||||||
val signers = inputs[0] as? List<List<PublicKey>> ?: emptyList()
|
val signersProvider = inputs[0] as? Supplier<List<List<PublicKey>>> ?: Supplier(::emptyList)
|
||||||
val commandsData = inputs[1] as? List<CommandData> ?: emptyList()
|
val commandsDataProvider = inputs[1] as? Supplier<List<CommandData>> ?: Supplier(::emptyList)
|
||||||
val partialMerkleLeafIndices = inputs[2] as? IntArray
|
val partialMerkleLeafIndices = inputs[2] as? IntArray
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This logic has been lovingly reproduced from [net.corda.core.internal.deserialiseCommands].
|
* This logic has been lovingly reproduced from [net.corda.core.internal.deserialiseCommands].
|
||||||
*/
|
*/
|
||||||
return if (partialMerkleLeafIndices != null) {
|
return Supplier {
|
||||||
check(commandsData.size <= signers.size) {
|
val signers = signersProvider.get()
|
||||||
"Invalid Transaction. Fewer Signers (${signers.size}) than CommandData (${commandsData.size}) objects"
|
val commandsData = commandsDataProvider.get()
|
||||||
}
|
|
||||||
if (partialMerkleLeafIndices.isNotEmpty()) {
|
if (partialMerkleLeafIndices != null) {
|
||||||
check(partialMerkleLeafIndices.max()!! < signers.size) {
|
check(commandsData.size <= signers.size) {
|
||||||
"Invalid Transaction. A command with no corresponding signer detected"
|
"Invalid Transaction. Fewer Signers (${signers.size}) than CommandData (${commandsData.size}) objects"
|
||||||
|
}
|
||||||
|
if (partialMerkleLeafIndices.isNotEmpty()) {
|
||||||
|
check(partialMerkleLeafIndices.max()!! < signers.size) {
|
||||||
|
"Invalid Transaction. A command with no corresponding signer detected"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
commandsData.lazyMapped { commandData, index ->
|
||||||
|
// Deprecated signingParties property not supported.
|
||||||
|
CommandWithParties(signers[partialMerkleLeafIndices[index]], emptyList(), commandData)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
check(commandsData.size == signers.size) {
|
||||||
|
"Invalid Transaction. Sizes of CommandData (${commandsData.size}) and Signers (${signers.size}) do not match"
|
||||||
|
}
|
||||||
|
commandsData.lazyMapped { commandData, index ->
|
||||||
|
// Deprecated signingParties property not supported.
|
||||||
|
CommandWithParties(signers[index], emptyList(), commandData)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
commandsData.lazyMapped { commandData, index ->
|
|
||||||
CommandWithParties(signers[partialMerkleLeafIndices[index]], emptyList(), commandData)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
check(commandsData.size == signers.size) {
|
|
||||||
"Invalid Transaction. Sizes of CommandData (${commandsData.size}) and Signers (${signers.size}) do not match"
|
|
||||||
}
|
|
||||||
commandsData.lazyMapped { commandData, index ->
|
|
||||||
CommandWithParties(signers[index], emptyList(), commandData)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,19 +5,22 @@ import net.corda.core.internal.TransactionDeserialisationException
|
|||||||
import net.corda.core.internal.lazyMapped
|
import net.corda.core.internal.lazyMapped
|
||||||
import net.corda.core.utilities.OpaqueBytes
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
import java.util.function.Function
|
import java.util.function.Function
|
||||||
|
import java.util.function.Supplier
|
||||||
|
|
||||||
class ComponentBuilder : Function<Array<Any?>, List<*>> {
|
class ComponentBuilder : Function<Array<Any?>, Supplier<List<*>>> {
|
||||||
@Suppress("unchecked_cast", "TooGenericExceptionCaught")
|
@Suppress("unchecked_cast", "TooGenericExceptionCaught")
|
||||||
override fun apply(inputs: Array<Any?>): List<*> {
|
override fun apply(inputs: Array<Any?>): Supplier<List<*>> {
|
||||||
val deserializer = inputs[0] as Function<in Any?, out Any?>
|
val deserializer = inputs[0] as Function<in Any?, out Any?>
|
||||||
val groupType = inputs[1] as ComponentGroupEnum
|
val groupType = inputs[1] as ComponentGroupEnum
|
||||||
val components = (inputs[2] as Array<ByteArray>).map(::OpaqueBytes)
|
val components = (inputs[2] as Array<ByteArray>).map(::OpaqueBytes)
|
||||||
|
|
||||||
return components.lazyMapped { component, index ->
|
return Supplier {
|
||||||
try {
|
components.lazyMapped { component, index ->
|
||||||
deserializer.apply(component.bytes)
|
try {
|
||||||
} catch (e: Exception) {
|
deserializer.apply(component.bytes)
|
||||||
throw TransactionDeserialisationException(groupType, index, e)
|
} catch (e: Exception) {
|
||||||
|
throw TransactionDeserialisationException(groupType, index, e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,54 +0,0 @@
|
|||||||
@file:JvmName("LtxConstants")
|
|
||||||
package net.corda.node.djvm
|
|
||||||
|
|
||||||
import net.corda.core.contracts.Attachment
|
|
||||||
import net.corda.core.contracts.CommandData
|
|
||||||
import net.corda.core.contracts.CommandWithParties
|
|
||||||
import net.corda.core.contracts.ContractState
|
|
||||||
import net.corda.core.contracts.PrivacySalt
|
|
||||||
import net.corda.core.contracts.StateAndRef
|
|
||||||
import net.corda.core.contracts.StateRef
|
|
||||||
import net.corda.core.contracts.TimeWindow
|
|
||||||
import net.corda.core.contracts.TransactionState
|
|
||||||
import net.corda.core.crypto.DigestService
|
|
||||||
import net.corda.core.crypto.SecureHash
|
|
||||||
import net.corda.core.identity.Party
|
|
||||||
import net.corda.core.node.NetworkParameters
|
|
||||||
import net.corda.core.transactions.LedgerTransaction
|
|
||||||
import java.util.function.Function
|
|
||||||
|
|
||||||
private const val TX_INPUTS = 0
|
|
||||||
private const val TX_OUTPUTS = 1
|
|
||||||
private const val TX_COMMANDS = 2
|
|
||||||
private const val TX_ATTACHMENTS = 3
|
|
||||||
private const val TX_ID = 4
|
|
||||||
private const val TX_NOTARY = 5
|
|
||||||
private const val TX_TIME_WINDOW = 6
|
|
||||||
private const val TX_PRIVACY_SALT = 7
|
|
||||||
private const val TX_NETWORK_PARAMETERS = 8
|
|
||||||
private const val TX_REFERENCES = 9
|
|
||||||
private const val TX_DIGEST_SERVICE = 10
|
|
||||||
|
|
||||||
class LtxFactory : Function<Array<out Any?>, LedgerTransaction> {
|
|
||||||
|
|
||||||
@Suppress("unchecked_cast")
|
|
||||||
override fun apply(txArgs: Array<out Any?>): LedgerTransaction {
|
|
||||||
return LedgerTransaction.createForSandbox(
|
|
||||||
inputs = (txArgs[TX_INPUTS] as Array<Array<out Any?>>).map { it.toStateAndRef() },
|
|
||||||
outputs = (txArgs[TX_OUTPUTS] as? List<TransactionState<ContractState>>) ?: emptyList(),
|
|
||||||
commands = (txArgs[TX_COMMANDS] as? List<CommandWithParties<CommandData>>) ?: emptyList(),
|
|
||||||
attachments = (txArgs[TX_ATTACHMENTS] as? List<Attachment>) ?: emptyList(),
|
|
||||||
id = txArgs[TX_ID] as SecureHash,
|
|
||||||
notary = txArgs[TX_NOTARY] as? Party,
|
|
||||||
timeWindow = txArgs[TX_TIME_WINDOW] as? TimeWindow,
|
|
||||||
privacySalt = txArgs[TX_PRIVACY_SALT] as PrivacySalt,
|
|
||||||
networkParameters = txArgs[TX_NETWORK_PARAMETERS] as NetworkParameters,
|
|
||||||
references = (txArgs[TX_REFERENCES] as Array<Array<out Any?>>).map { it.toStateAndRef() },
|
|
||||||
digestService = if (txArgs.size > TX_DIGEST_SERVICE) (txArgs[TX_DIGEST_SERVICE] as DigestService) else DigestService.sha2_256
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Array<*>.toStateAndRef(): StateAndRef<ContractState> {
|
|
||||||
return StateAndRef(this[0] as TransactionState<*>, this[1] as StateRef)
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,73 @@
|
|||||||
|
@file:JvmName("LtxTools")
|
||||||
|
package net.corda.node.djvm
|
||||||
|
|
||||||
|
import net.corda.core.contracts.Attachment
|
||||||
|
import net.corda.core.contracts.CommandData
|
||||||
|
import net.corda.core.contracts.CommandWithParties
|
||||||
|
import net.corda.core.contracts.ContractState
|
||||||
|
import net.corda.core.contracts.PrivacySalt
|
||||||
|
import net.corda.core.contracts.StateAndRef
|
||||||
|
import net.corda.core.contracts.StateRef
|
||||||
|
import net.corda.core.contracts.TimeWindow
|
||||||
|
import net.corda.core.contracts.TransactionState
|
||||||
|
import net.corda.core.crypto.DigestService
|
||||||
|
import net.corda.core.crypto.SecureHash
|
||||||
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.node.NetworkParameters
|
||||||
|
import net.corda.core.transactions.LedgerTransaction
|
||||||
|
import java.util.function.Function
|
||||||
|
import java.util.function.Supplier
|
||||||
|
|
||||||
|
private const val TX_INPUTS = 0
|
||||||
|
private const val TX_OUTPUTS = 1
|
||||||
|
private const val TX_COMMANDS = 2
|
||||||
|
private const val TX_ATTACHMENTS = 3
|
||||||
|
private const val TX_ID = 4
|
||||||
|
private const val TX_NOTARY = 5
|
||||||
|
private const val TX_TIME_WINDOW = 6
|
||||||
|
private const val TX_PRIVACY_SALT = 7
|
||||||
|
private const val TX_NETWORK_PARAMETERS = 8
|
||||||
|
private const val TX_REFERENCES = 9
|
||||||
|
private const val TX_DIGEST_SERVICE = 10
|
||||||
|
|
||||||
|
class LtxSupplierFactory : Function<Array<out Any?>, Supplier<LedgerTransaction>> {
|
||||||
|
@Suppress("unchecked_cast")
|
||||||
|
override fun apply(txArgs: Array<out Any?>): Supplier<LedgerTransaction> {
|
||||||
|
val inputProvider = (txArgs[TX_INPUTS] as Function<in Any?, Array<Array<out Any?>>>)
|
||||||
|
.andThen(Function(Array<Array<out Any?>>::toContractStatesAndRef))
|
||||||
|
.toSupplier()
|
||||||
|
val outputProvider = txArgs[TX_OUTPUTS] as? Supplier<List<TransactionState<ContractState>>> ?: Supplier(::emptyList)
|
||||||
|
val commandsProvider = txArgs[TX_COMMANDS] as Supplier<List<CommandWithParties<CommandData>>>
|
||||||
|
val referencesProvider = (txArgs[TX_REFERENCES] as Function<in Any?, Array<Array<out Any?>>>)
|
||||||
|
.andThen(Function(Array<Array<out Any?>>::toContractStatesAndRef))
|
||||||
|
.toSupplier()
|
||||||
|
val networkParameters = (txArgs[TX_NETWORK_PARAMETERS] as? NetworkParameters)?.toImmutable()
|
||||||
|
return Supplier {
|
||||||
|
LedgerTransaction.createForContractVerify(
|
||||||
|
inputs = inputProvider.get(),
|
||||||
|
outputs = outputProvider.get(),
|
||||||
|
commands = commandsProvider.get(),
|
||||||
|
attachments = txArgs[TX_ATTACHMENTS] as? List<Attachment> ?: emptyList(),
|
||||||
|
id = txArgs[TX_ID] as SecureHash,
|
||||||
|
notary = txArgs[TX_NOTARY] as? Party,
|
||||||
|
timeWindow = txArgs[TX_TIME_WINDOW] as? TimeWindow,
|
||||||
|
privacySalt = txArgs[TX_PRIVACY_SALT] as PrivacySalt,
|
||||||
|
networkParameters = networkParameters,
|
||||||
|
references = referencesProvider.get(),
|
||||||
|
digestService = txArgs[TX_DIGEST_SERVICE] as DigestService
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T> Function<in Any?, T>.toSupplier(): Supplier<T> {
|
||||||
|
return Supplier { apply(null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Array<Array<out Any?>>.toContractStatesAndRef(): List<StateAndRef<ContractState>> {
|
||||||
|
return map(Array<out Any?>::toStateAndRef)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Array<*>.toStateAndRef(): StateAndRef<ContractState> {
|
||||||
|
return StateAndRef(this[0] as TransactionState<*>, this[1] as StateRef)
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
package net.corda.contracts.multiple.evil
|
||||||
|
|
||||||
|
import net.corda.contracts.multiple.vulnerable.MutableDataObject
|
||||||
|
import net.corda.contracts.multiple.vulnerable.VulnerablePaymentContract.VulnerablePurchase
|
||||||
|
import net.corda.contracts.multiple.vulnerable.VulnerablePaymentContract.VulnerableState
|
||||||
|
import net.corda.core.contracts.CommandData
|
||||||
|
import net.corda.core.contracts.Contract
|
||||||
|
import net.corda.core.contracts.ContractState
|
||||||
|
import net.corda.core.identity.AbstractParty
|
||||||
|
import net.corda.core.transactions.LedgerTransaction
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
class EvilContract : Contract {
|
||||||
|
override fun verify(tx: LedgerTransaction) {
|
||||||
|
val vulnerableStates = tx.outputsOfType(VulnerableState::class.java)
|
||||||
|
val vulnerablePurchases = tx.commandsOfType(VulnerablePurchase::class.java)
|
||||||
|
|
||||||
|
val addExtras = tx.commandsOfType(AddExtra::class.java)
|
||||||
|
addExtras.forEach { extra ->
|
||||||
|
val extraValue = extra.value.payment.value
|
||||||
|
|
||||||
|
// And our extra value to every vulnerable output state.
|
||||||
|
vulnerableStates.forEach { state ->
|
||||||
|
state.data?.also { data ->
|
||||||
|
data.value += extraValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add our extra value to every vulnerable command too.
|
||||||
|
vulnerablePurchases.forEach { purchase ->
|
||||||
|
purchase.value.payment.value += extraValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EvilState(val owner: AbstractParty) : ContractState {
|
||||||
|
override val participants: List<AbstractParty> = listOf(owner)
|
||||||
|
|
||||||
|
@Override
|
||||||
|
override fun toString(): String {
|
||||||
|
return "Money For Nothing!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AddExtra(val payment: MutableDataObject) : CommandData
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
package net.corda.contracts.multiple.vulnerable
|
||||||
|
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
|
|
||||||
|
@CordaSerializable
|
||||||
|
data class MutableDataObject(var value: Long) : Comparable<MutableDataObject> {
|
||||||
|
override fun toString(): String {
|
||||||
|
return "$value data points"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun compareTo(other: MutableDataObject): Int {
|
||||||
|
return value.compareTo(other.value)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
package net.corda.contracts.multiple.vulnerable
|
||||||
|
|
||||||
|
import net.corda.core.contracts.CommandData
|
||||||
|
import net.corda.core.contracts.Contract
|
||||||
|
import net.corda.core.contracts.ContractState
|
||||||
|
import net.corda.core.contracts.requireThat
|
||||||
|
import net.corda.core.identity.AbstractParty
|
||||||
|
import net.corda.core.transactions.LedgerTransaction
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
class VulnerablePaymentContract : Contract {
|
||||||
|
companion object {
|
||||||
|
const val BASE_PAYMENT = 2000L
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun verify(tx: LedgerTransaction) {
|
||||||
|
val states = tx.outputsOfType<VulnerableState>()
|
||||||
|
requireThat {
|
||||||
|
"Requires at least one data state" using states.isNotEmpty()
|
||||||
|
}
|
||||||
|
val purchases = tx.commandsOfType<VulnerablePurchase>()
|
||||||
|
requireThat {
|
||||||
|
"Requires at least one purchase" using purchases.isNotEmpty()
|
||||||
|
}
|
||||||
|
for (purchase in purchases) {
|
||||||
|
val payment = purchase.value.payment
|
||||||
|
requireThat {
|
||||||
|
"Purchase payment of $payment should be at least $BASE_PAYMENT" using (payment.value >= BASE_PAYMENT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class VulnerableState(val owner: AbstractParty, val data: MutableDataObject?) : ContractState {
|
||||||
|
override val participants: List<AbstractParty> = listOf(owner)
|
||||||
|
|
||||||
|
@Override
|
||||||
|
override fun toString(): String {
|
||||||
|
return data.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class VulnerablePurchase(val payment: MutableDataObject) : CommandData
|
||||||
|
}
|
@ -0,0 +1,113 @@
|
|||||||
|
package net.corda.contracts.mutator
|
||||||
|
|
||||||
|
import net.corda.core.contracts.CommandData
|
||||||
|
import net.corda.core.contracts.CommandWithParties
|
||||||
|
import net.corda.core.contracts.Contract
|
||||||
|
import net.corda.core.contracts.ContractState
|
||||||
|
import net.corda.core.contracts.TransactionState
|
||||||
|
import net.corda.core.contracts.requireSingleCommand
|
||||||
|
import net.corda.core.contracts.requireThat
|
||||||
|
import net.corda.core.identity.AbstractParty
|
||||||
|
import net.corda.core.internal.Verifier
|
||||||
|
import net.corda.core.serialization.SerializationContext
|
||||||
|
import net.corda.core.transactions.LedgerTransaction
|
||||||
|
|
||||||
|
class MutatorContract : Contract {
|
||||||
|
override fun verify(tx: LedgerTransaction) {
|
||||||
|
tx.transform { componentGroups, serializedInputs, serializedReferences ->
|
||||||
|
requireThat {
|
||||||
|
"component groups are protected" using componentGroups.isImmutableAnd(isEmpty = true)
|
||||||
|
"serialized inputs are protected" using serializedInputs.isImmutableAnd(isEmpty = true)
|
||||||
|
"serialized references are protected" using serializedReferences.isImmutableAnd(isEmpty = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requireThat {
|
||||||
|
"Cannot add/remove inputs" using tx.inputs.isImmutable()
|
||||||
|
"Cannot add/remove outputs" using failToMutateOutputs(tx)
|
||||||
|
"Cannot add/remove commands" using failToMutateCommands(tx)
|
||||||
|
"Cannot add/remove references" using tx.references.isImmutable()
|
||||||
|
"Cannot add/remove attachments" using tx.attachments.isImmutableAnd(isEmpty = false)
|
||||||
|
"Cannot specialise transaction" using failToSpecialise(tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
requireNotNull(tx.networkParameters).also { networkParameters ->
|
||||||
|
requireThat {
|
||||||
|
"Cannot add/remove notaries" using networkParameters.notaries.isImmutableAnd(isEmpty = false)
|
||||||
|
"Cannot add/remove package ownerships" using networkParameters.packageOwnership.isImmutable()
|
||||||
|
"Cannot add/remove whitelisted contracts" using networkParameters.whitelistedContractImplementations.isImmutable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<*>.isImmutableAnd(isEmpty: Boolean): Boolean {
|
||||||
|
return isImmutable() && (this.isEmpty() == isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<*>.isImmutable(): Boolean {
|
||||||
|
return try {
|
||||||
|
@Suppress("platform_class_mapped_to_kotlin")
|
||||||
|
(this as java.util.List<*>).clear()
|
||||||
|
false
|
||||||
|
} catch (e: UnsupportedOperationException) {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun failToMutateOutputs(tx: LedgerTransaction): Boolean {
|
||||||
|
val output = tx.outputsOfType<MutateState>().single()
|
||||||
|
val mutableOutputs = tx.outputs as MutableList<in TransactionState<ContractState>>
|
||||||
|
return try {
|
||||||
|
mutableOutputs += TransactionState(MutateState(output.owner), MutatorContract::class.java.name, tx.notary!!, 0)
|
||||||
|
false
|
||||||
|
} catch (e: UnsupportedOperationException) {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun failToMutateCommands(tx: LedgerTransaction): Boolean {
|
||||||
|
val mutate = tx.commands.requireSingleCommand<MutateCommand>()
|
||||||
|
val mutableCommands = tx.commands as MutableList<in CommandWithParties<CommandData>>
|
||||||
|
return try {
|
||||||
|
mutableCommands += CommandWithParties(mutate.signers, emptyList(), MutateCommand())
|
||||||
|
false
|
||||||
|
} catch (e: UnsupportedOperationException) {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Map<*, *>.isImmutable(): Boolean {
|
||||||
|
return try {
|
||||||
|
@Suppress("platform_class_mapped_to_kotlin")
|
||||||
|
(this as java.util.Map<*, *>).clear()
|
||||||
|
false
|
||||||
|
} catch (e: UnsupportedOperationException) {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun failToSpecialise(ltx: LedgerTransaction): Boolean {
|
||||||
|
return try {
|
||||||
|
ltx.specialise(::ExtraSpecialise)
|
||||||
|
false
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ExtraSpecialise(ltx: LedgerTransaction, ctx: SerializationContext)
|
||||||
|
: Verifier(ltx, ctx.deserializationClassLoader) {
|
||||||
|
override fun verifyContracts() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MutateState(val owner: AbstractParty) : ContractState {
|
||||||
|
override val participants: List<AbstractParty> = listOf(owner)
|
||||||
|
|
||||||
|
@Override
|
||||||
|
override fun toString(): String {
|
||||||
|
return "All change!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MutateCommand : CommandData
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
package net.corda.flows.multiple.evil
|
||||||
|
|
||||||
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
import net.corda.contracts.multiple.evil.EvilContract.EvilState
|
||||||
|
import net.corda.contracts.multiple.evil.EvilContract.AddExtra
|
||||||
|
import net.corda.contracts.multiple.vulnerable.MutableDataObject
|
||||||
|
import net.corda.contracts.multiple.vulnerable.VulnerablePaymentContract.VulnerablePurchase
|
||||||
|
import net.corda.contracts.multiple.vulnerable.VulnerablePaymentContract.VulnerableState
|
||||||
|
import net.corda.core.contracts.Command
|
||||||
|
import net.corda.core.crypto.SecureHash
|
||||||
|
import net.corda.core.flows.FlowLogic
|
||||||
|
import net.corda.core.flows.StartableByRPC
|
||||||
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
|
|
||||||
|
@StartableByRPC
|
||||||
|
class EvilFlow(
|
||||||
|
private val purchase: MutableDataObject
|
||||||
|
) : FlowLogic<SecureHash>() {
|
||||||
|
private companion object {
|
||||||
|
private val NOTHING = MutableDataObject(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suspendable
|
||||||
|
override fun call(): SecureHash {
|
||||||
|
val notary = serviceHub.networkMapCache.notaryIdentities[0]
|
||||||
|
val stx = serviceHub.signInitialTransaction(
|
||||||
|
TransactionBuilder(notary)
|
||||||
|
// Add Evil objects first, so that Corda will verify EvilContract first.
|
||||||
|
.addCommand(Command(AddExtra(purchase), ourIdentity.owningKey))
|
||||||
|
.addOutputState(EvilState(ourIdentity))
|
||||||
|
|
||||||
|
// Now add the VulnerablePaymentContract objects with NO PAYMENT!
|
||||||
|
.addCommand(Command(VulnerablePurchase(NOTHING), ourIdentity.owningKey))
|
||||||
|
.addOutputState(VulnerableState(ourIdentity, NOTHING))
|
||||||
|
)
|
||||||
|
stx.verify(serviceHub, checkSufficientSignatures = false)
|
||||||
|
return stx.id
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
package net.corda.flows.mutator
|
||||||
|
|
||||||
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
import net.corda.contracts.mutator.MutatorContract.MutateCommand
|
||||||
|
import net.corda.contracts.mutator.MutatorContract.MutateState
|
||||||
|
import net.corda.core.contracts.Command
|
||||||
|
import net.corda.core.crypto.SecureHash
|
||||||
|
import net.corda.core.flows.FlowLogic
|
||||||
|
import net.corda.core.flows.StartableByRPC
|
||||||
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
|
|
||||||
|
@StartableByRPC
|
||||||
|
class MutatorFlow : FlowLogic<SecureHash>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call(): SecureHash {
|
||||||
|
val notary = serviceHub.networkMapCache.notaryIdentities[0]
|
||||||
|
val stx = serviceHub.signInitialTransaction(
|
||||||
|
TransactionBuilder(notary)
|
||||||
|
// Create some content for the LedgerTransaction.
|
||||||
|
.addOutputState(MutateState(ourIdentity))
|
||||||
|
.addCommand(Command(MutateCommand(), ourIdentity.owningKey))
|
||||||
|
)
|
||||||
|
stx.verify(serviceHub, checkSufficientSignatures = false)
|
||||||
|
return stx.id
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
package net.corda.node
|
||||||
|
|
||||||
|
import net.corda.client.rpc.CordaRPCClient
|
||||||
|
import net.corda.core.messaging.startFlow
|
||||||
|
import net.corda.core.utilities.getOrThrow
|
||||||
|
import net.corda.core.utilities.loggerFor
|
||||||
|
import net.corda.flows.mutator.MutatorFlow
|
||||||
|
import net.corda.node.services.Permissions
|
||||||
|
import net.corda.testing.core.ALICE_NAME
|
||||||
|
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
||||||
|
import net.corda.testing.driver.DriverParameters
|
||||||
|
import net.corda.testing.driver.driver
|
||||||
|
import net.corda.testing.driver.internal.incrementalPortAllocation
|
||||||
|
import net.corda.testing.node.NotarySpec
|
||||||
|
import net.corda.testing.node.User
|
||||||
|
import net.corda.testing.node.internal.cordappWithPackages
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ContractCannotMutateTransactionTest {
|
||||||
|
companion object {
|
||||||
|
private val logger = loggerFor<ContractCannotMutateTransactionTest>()
|
||||||
|
private val user = User("u", "p", setOf(Permissions.all()))
|
||||||
|
private val mutatorFlowCorDapp = cordappWithPackages("net.corda.flows.mutator").signed()
|
||||||
|
private val mutatorContractCorDapp = cordappWithPackages("net.corda.contracts.mutator").signed()
|
||||||
|
|
||||||
|
fun driverParameters(runInProcess: Boolean): DriverParameters {
|
||||||
|
return DriverParameters(
|
||||||
|
portAllocation = incrementalPortAllocation(),
|
||||||
|
startNodesInProcess = runInProcess,
|
||||||
|
notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, startInProcess = runInProcess, validating = true)),
|
||||||
|
cordappsForAllNodes = listOf(mutatorContractCorDapp, mutatorFlowCorDapp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(timeout = 300_000)
|
||||||
|
fun testContractCannotModifyTransaction() {
|
||||||
|
driver(driverParameters(runInProcess = false)) {
|
||||||
|
val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
|
||||||
|
val txID = CordaRPCClient(hostAndPort = alice.rpcAddress)
|
||||||
|
.start(user.username, user.password)
|
||||||
|
.use { client ->
|
||||||
|
client.proxy.startFlow(::MutatorFlow).returnValue.getOrThrow()
|
||||||
|
}
|
||||||
|
logger.info("TX-ID: {}", txID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,60 @@
|
|||||||
|
package net.corda.node
|
||||||
|
|
||||||
|
import net.corda.client.rpc.CordaRPCClient
|
||||||
|
import net.corda.contracts.multiple.vulnerable.MutableDataObject
|
||||||
|
import net.corda.core.contracts.TransactionVerificationException.ContractRejection
|
||||||
|
import net.corda.core.messaging.startFlow
|
||||||
|
import net.corda.core.utilities.getOrThrow
|
||||||
|
import net.corda.flows.multiple.evil.EvilFlow
|
||||||
|
import net.corda.node.services.Permissions
|
||||||
|
import net.corda.testing.core.ALICE_NAME
|
||||||
|
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
||||||
|
import net.corda.testing.driver.DriverParameters
|
||||||
|
import net.corda.testing.driver.driver
|
||||||
|
import net.corda.testing.driver.internal.incrementalPortAllocation
|
||||||
|
import net.corda.testing.node.NotarySpec
|
||||||
|
import net.corda.testing.node.User
|
||||||
|
import net.corda.testing.node.internal.cordappWithPackages
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.junit.Test
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
|
||||||
|
class EvilContractCannotModifyStatesTest {
|
||||||
|
companion object {
|
||||||
|
private val user = User("u", "p", setOf(Permissions.all()))
|
||||||
|
private val evilFlowCorDapp = cordappWithPackages("net.corda.flows.multiple.evil").signed()
|
||||||
|
private val evilContractCorDapp = cordappWithPackages("net.corda.contracts.multiple.evil").signed()
|
||||||
|
private val vulnerableContractCorDapp = cordappWithPackages("net.corda.contracts.multiple.vulnerable").signed()
|
||||||
|
|
||||||
|
private val NOTHING = MutableDataObject(0)
|
||||||
|
|
||||||
|
fun driverParameters(runInProcess: Boolean): DriverParameters {
|
||||||
|
return DriverParameters(
|
||||||
|
portAllocation = incrementalPortAllocation(),
|
||||||
|
startNodesInProcess = runInProcess,
|
||||||
|
notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, startInProcess = runInProcess, validating = true)),
|
||||||
|
cordappsForAllNodes = listOf(
|
||||||
|
vulnerableContractCorDapp,
|
||||||
|
evilContractCorDapp,
|
||||||
|
evilFlowCorDapp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(timeout = 300_000)
|
||||||
|
fun testContractThatTriesToModifyStates() {
|
||||||
|
val evilData = MutableDataObject(5000)
|
||||||
|
driver(driverParameters(runInProcess = false)) {
|
||||||
|
val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
|
||||||
|
val ex = assertFailsWith<ContractRejection> {
|
||||||
|
CordaRPCClient(hostAndPort = alice.rpcAddress)
|
||||||
|
.start(user.username, user.password)
|
||||||
|
.use { client ->
|
||||||
|
client.proxy.startFlow(::EvilFlow, evilData).returnValue.getOrThrow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertThat(ex).hasMessageContaining("Purchase payment of $NOTHING should be at least ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
package net.corda.node.services
|
||||||
|
|
||||||
|
import net.corda.client.rpc.CordaRPCClient
|
||||||
|
import net.corda.core.messaging.startFlow
|
||||||
|
import net.corda.core.utilities.getOrThrow
|
||||||
|
import net.corda.core.utilities.loggerFor
|
||||||
|
import net.corda.flows.mutator.MutatorFlow
|
||||||
|
import net.corda.node.DeterministicSourcesRule
|
||||||
|
import net.corda.testing.core.ALICE_NAME
|
||||||
|
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
||||||
|
import net.corda.testing.driver.DriverParameters
|
||||||
|
import net.corda.testing.driver.driver
|
||||||
|
import net.corda.testing.driver.internal.incrementalPortAllocation
|
||||||
|
import net.corda.testing.node.NotarySpec
|
||||||
|
import net.corda.testing.node.User
|
||||||
|
import net.corda.testing.node.internal.cordappWithPackages
|
||||||
|
import org.junit.ClassRule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class DeterministicContractCannotMutateTransactionTest {
|
||||||
|
companion object {
|
||||||
|
private val logger = loggerFor<DeterministicContractCannotMutateTransactionTest>()
|
||||||
|
private val user = User("u", "p", setOf(Permissions.all()))
|
||||||
|
private val mutatorFlowCorDapp = cordappWithPackages("net.corda.flows.mutator").signed()
|
||||||
|
private val mutatorContractCorDapp = cordappWithPackages("net.corda.contracts.mutator").signed()
|
||||||
|
|
||||||
|
@ClassRule
|
||||||
|
@JvmField
|
||||||
|
val djvmSources = DeterministicSourcesRule()
|
||||||
|
|
||||||
|
fun driverParameters(runInProcess: Boolean): DriverParameters {
|
||||||
|
return DriverParameters(
|
||||||
|
portAllocation = incrementalPortAllocation(),
|
||||||
|
startNodesInProcess = runInProcess,
|
||||||
|
notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, startInProcess = runInProcess, validating = true)),
|
||||||
|
cordappsForAllNodes = listOf(mutatorContractCorDapp, mutatorFlowCorDapp),
|
||||||
|
djvmBootstrapSource = djvmSources.bootstrap,
|
||||||
|
djvmCordaSource = djvmSources.corda
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(timeout = 300_000)
|
||||||
|
fun testContractCannotModifyTransaction() {
|
||||||
|
driver(driverParameters(runInProcess = false)) {
|
||||||
|
val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
|
||||||
|
val txID = CordaRPCClient(hostAndPort = alice.rpcAddress)
|
||||||
|
.start(user.username, user.password)
|
||||||
|
.use { client ->
|
||||||
|
client.proxy.startFlow(::MutatorFlow).returnValue.getOrThrow()
|
||||||
|
}
|
||||||
|
logger.info("TX-ID: {}", txID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
package net.corda.node.services
|
||||||
|
|
||||||
|
import net.corda.client.rpc.CordaRPCClient
|
||||||
|
import net.corda.contracts.multiple.vulnerable.MutableDataObject
|
||||||
|
import net.corda.contracts.multiple.vulnerable.VulnerablePaymentContract
|
||||||
|
import net.corda.core.messaging.startFlow
|
||||||
|
import net.corda.core.utilities.getOrThrow
|
||||||
|
import net.corda.flows.multiple.evil.EvilFlow
|
||||||
|
import net.corda.node.DeterministicSourcesRule
|
||||||
|
import net.corda.node.internal.djvm.DeterministicVerificationException
|
||||||
|
import net.corda.testing.core.ALICE_NAME
|
||||||
|
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
||||||
|
import net.corda.testing.driver.DriverParameters
|
||||||
|
import net.corda.testing.driver.driver
|
||||||
|
import net.corda.testing.driver.internal.incrementalPortAllocation
|
||||||
|
import net.corda.testing.node.NotarySpec
|
||||||
|
import net.corda.testing.node.User
|
||||||
|
import net.corda.testing.node.internal.cordappWithPackages
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.junit.ClassRule
|
||||||
|
import org.junit.Test
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
|
||||||
|
class DeterministicEvilContractCannotModifyStatesTest {
|
||||||
|
companion object {
|
||||||
|
private val user = User("u", "p", setOf(Permissions.all()))
|
||||||
|
private val evilFlowCorDapp = cordappWithPackages("net.corda.flows.multiple.evil").signed()
|
||||||
|
private val evilContractCorDapp = cordappWithPackages("net.corda.contracts.multiple.evil").signed()
|
||||||
|
private val vulnerableContractCorDapp = cordappWithPackages("net.corda.contracts.multiple.vulnerable").signed()
|
||||||
|
|
||||||
|
private val NOTHING = MutableDataObject(0)
|
||||||
|
|
||||||
|
@ClassRule
|
||||||
|
@JvmField
|
||||||
|
val djvmSources = DeterministicSourcesRule()
|
||||||
|
|
||||||
|
fun driverParameters(runInProcess: Boolean): DriverParameters {
|
||||||
|
return DriverParameters(
|
||||||
|
portAllocation = incrementalPortAllocation(),
|
||||||
|
startNodesInProcess = runInProcess,
|
||||||
|
notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, startInProcess = runInProcess, validating = true)),
|
||||||
|
cordappsForAllNodes = listOf(
|
||||||
|
vulnerableContractCorDapp,
|
||||||
|
evilContractCorDapp,
|
||||||
|
evilFlowCorDapp
|
||||||
|
),
|
||||||
|
djvmBootstrapSource = djvmSources.bootstrap,
|
||||||
|
djvmCordaSource = djvmSources.corda
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(timeout = 300_000)
|
||||||
|
fun testContractThatTriesToModifyStates() {
|
||||||
|
val evilData = MutableDataObject(5000)
|
||||||
|
driver(driverParameters(runInProcess = false)) {
|
||||||
|
val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
|
||||||
|
val ex = assertFailsWith<DeterministicVerificationException> {
|
||||||
|
CordaRPCClient(hostAndPort = alice.rpcAddress)
|
||||||
|
.start(user.username, user.password)
|
||||||
|
.use { client ->
|
||||||
|
client.proxy.startFlow(::EvilFlow, evilData).returnValue.getOrThrow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertThat(ex)
|
||||||
|
.hasMessageStartingWith("sandbox.net.corda.core.contracts.TransactionVerificationException\$ContractRejection -> ")
|
||||||
|
.hasMessageContaining(" Contract verification failed: Failed requirement: Purchase payment of $NOTHING should be at least ")
|
||||||
|
.hasMessageContaining(", contract: sandbox.${VulnerablePaymentContract::class.java.name}, ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,7 +5,6 @@ import net.corda.contracts.djvm.attachment.SandboxAttachmentContract.ExtractFile
|
|||||||
import net.corda.core.messaging.startFlow
|
import net.corda.core.messaging.startFlow
|
||||||
import net.corda.core.utilities.getOrThrow
|
import net.corda.core.utilities.getOrThrow
|
||||||
import net.corda.core.utilities.loggerFor
|
import net.corda.core.utilities.loggerFor
|
||||||
import net.corda.djvm.code.asResourcePath
|
|
||||||
import net.corda.flows.djvm.attachment.SandboxAttachmentFlow
|
import net.corda.flows.djvm.attachment.SandboxAttachmentFlow
|
||||||
import net.corda.node.DeterministicSourcesRule
|
import net.corda.node.DeterministicSourcesRule
|
||||||
import net.corda.node.internal.djvm.DeterministicVerificationException
|
import net.corda.node.internal.djvm.DeterministicVerificationException
|
||||||
@ -52,7 +51,7 @@ class SandboxAttachmentsTest {
|
|||||||
|
|
||||||
@Test(timeout=300_000)
|
@Test(timeout=300_000)
|
||||||
fun `test attachment accessible within sandbox`() {
|
fun `test attachment accessible within sandbox`() {
|
||||||
val extractFile = ExtractFile(SandboxAttachmentContract::class.java.name.asResourcePath + ".class")
|
val extractFile = ExtractFile(SandboxAttachmentContract::class.java.name.replace('.', '/') + ".class")
|
||||||
driver(parametersFor(djvmSources)) {
|
driver(parametersFor(djvmSources)) {
|
||||||
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
|
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
|
||||||
val txId = assertDoesNotThrow {
|
val txId = assertDoesNotThrow {
|
||||||
|
@ -14,7 +14,7 @@ inline fun <reified A : Annotation> Class<*>.requireAnnotation(): A {
|
|||||||
|
|
||||||
fun scanForCustomSerializationScheme(className: String, classLoader: ClassLoader) : SerializationScheme {
|
fun scanForCustomSerializationScheme(className: String, classLoader: ClassLoader) : SerializationScheme {
|
||||||
val schemaClass = try {
|
val schemaClass = try {
|
||||||
classLoader.loadClass(className)
|
Class.forName(className, false, classLoader)
|
||||||
} catch (exception: ClassNotFoundException) {
|
} catch (exception: ClassNotFoundException) {
|
||||||
throw ConfigurationException("$className was declared as a custom serialization scheme but could not be found.")
|
throw ConfigurationException("$className was declared as a custom serialization scheme but could not be found.")
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ import net.corda.djvm.execution.SandboxException
|
|||||||
import net.corda.djvm.messages.Message
|
import net.corda.djvm.messages.Message
|
||||||
import net.corda.djvm.rewiring.SandboxClassLoader
|
import net.corda.djvm.rewiring.SandboxClassLoader
|
||||||
import net.corda.djvm.source.ClassSource
|
import net.corda.djvm.source.ClassSource
|
||||||
import net.corda.node.djvm.LtxFactory
|
import net.corda.node.djvm.LtxSupplierFactory
|
||||||
import java.util.function.Function
|
import java.util.function.Function
|
||||||
import kotlin.collections.LinkedHashSet
|
import kotlin.collections.LinkedHashSet
|
||||||
|
|
||||||
@ -93,14 +93,14 @@ class DeterministicVerifier(
|
|||||||
val networkingParametersData = ltx.networkParameters?.serialize()
|
val networkingParametersData = ltx.networkParameters?.serialize()
|
||||||
val digestServiceData = ltx.digestService.serialize()
|
val digestServiceData = ltx.digestService.serialize()
|
||||||
|
|
||||||
val createSandboxTx = taskFactory.apply(LtxFactory::class.java)
|
val createSandboxTx = taskFactory.apply(LtxSupplierFactory::class.java)
|
||||||
createSandboxTx.apply(arrayOf(
|
createSandboxTx.apply(arrayOf(
|
||||||
serializer.deserialize(serializedInputs),
|
classLoader.createForImport(Function { serializer.deserialize(serializedInputs) }),
|
||||||
componentFactory.toSandbox(OUTPUTS_GROUP, TransactionState::class.java),
|
componentFactory.toSandbox(OUTPUTS_GROUP, TransactionState::class.java),
|
||||||
CommandFactory(taskFactory).toSandbox(
|
CommandFactory(taskFactory).toSandbox(
|
||||||
componentFactory.toSandbox(SIGNERS_GROUP, List::class.java),
|
componentFactory.toSandbox(SIGNERS_GROUP, List::class.java),
|
||||||
componentFactory.toSandbox(COMMANDS_GROUP, CommandData::class.java),
|
componentFactory.toSandbox(COMMANDS_GROUP, CommandData::class.java),
|
||||||
componentFactory.calculateLeafIndicesFor(COMMANDS_GROUP, digestService = ltx.digestService)
|
componentFactory.calculateLeafIndicesFor(COMMANDS_GROUP, ltx.digestService)
|
||||||
),
|
),
|
||||||
attachmentFactory.toSandbox(ltx.attachments),
|
attachmentFactory.toSandbox(ltx.attachments),
|
||||||
serializer.deserialize(idData),
|
serializer.deserialize(idData),
|
||||||
@ -108,7 +108,7 @@ class DeterministicVerifier(
|
|||||||
serializer.deserialize(timeWindowData),
|
serializer.deserialize(timeWindowData),
|
||||||
serializer.deserialize(privacySaltData),
|
serializer.deserialize(privacySaltData),
|
||||||
serializer.deserialize(networkingParametersData),
|
serializer.deserialize(networkingParametersData),
|
||||||
serializer.deserialize(serializedReferences),
|
classLoader.createForImport(Function { serializer.deserialize(serializedReferences) }),
|
||||||
serializer.deserialize(digestServiceData)
|
serializer.deserialize(digestServiceData)
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
package net.corda.node.internal.djvm
|
package net.corda.node.internal.djvm
|
||||||
|
|
||||||
import net.corda.core.internal.SerializedStateAndRef
|
import net.corda.core.internal.SerializedStateAndRef
|
||||||
|
import net.corda.core.serialization.AMQP_ENVELOPE_CACHE_INITIAL_CAPACITY
|
||||||
|
import net.corda.core.serialization.AMQP_ENVELOPE_CACHE_PROPERTY
|
||||||
|
import net.corda.core.serialization.DESERIALIZATION_CACHE_PROPERTY
|
||||||
import net.corda.core.serialization.SerializationContext
|
import net.corda.core.serialization.SerializationContext
|
||||||
import net.corda.core.serialization.SerializationFactory
|
import net.corda.core.serialization.SerializationFactory
|
||||||
import net.corda.core.serialization.SerializedBytes
|
import net.corda.core.serialization.SerializedBytes
|
||||||
@ -22,14 +25,20 @@ class Serializer(
|
|||||||
init {
|
init {
|
||||||
val env = createSandboxSerializationEnv(classLoader, customSerializerNames, serializationWhitelists)
|
val env = createSandboxSerializationEnv(classLoader, customSerializerNames, serializationWhitelists)
|
||||||
factory = env.serializationFactory
|
factory = env.serializationFactory
|
||||||
context = env.p2pContext
|
context = env.p2pContext.withProperties(mapOf<Any, Any>(
|
||||||
|
// Duplicate the P2P SerializationContext and give it
|
||||||
|
// these extra properties, just for this transaction.
|
||||||
|
AMQP_ENVELOPE_CACHE_PROPERTY to HashMap<Any, Any>(AMQP_ENVELOPE_CACHE_INITIAL_CAPACITY),
|
||||||
|
DESERIALIZATION_CACHE_PROPERTY to HashMap<Any, Any>()
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a list of [SerializedStateAndRef] objects into arrays
|
* Convert a list of [SerializedStateAndRef] objects into arrays
|
||||||
* of deserialized sandbox objects. We will pass this array into
|
* of deserialized sandbox objects. We will pass this array into
|
||||||
* [net.corda.node.djvm.LtxFactory] to be transformed finally to
|
* [LtxSupplierFactory][net.corda.node.djvm.LtxSupplierFactory]
|
||||||
* a list of [net.corda.core.contracts.StateAndRef] objects,
|
* to be transformed finally to a list of
|
||||||
|
* [StateAndRef][net.corda.core.contracts.StateAndRef] objects,
|
||||||
*/
|
*/
|
||||||
fun deserialize(stateRefs: List<SerializedStateAndRef>): Array<Array<out Any?>> {
|
fun deserialize(stateRefs: List<SerializedStateAndRef>): Array<Array<out Any?>> {
|
||||||
return stateRefs.map {
|
return stateRefs.map {
|
||||||
|
@ -315,7 +315,7 @@ class CheckpointDumperImpl(private val checkpointStorage: CheckpointStorage, pri
|
|||||||
* the checkpoint agent source code
|
* the checkpoint agent source code
|
||||||
*/
|
*/
|
||||||
private fun checkpointAgentRunning() = try {
|
private fun checkpointAgentRunning() = try {
|
||||||
javaClass.classLoader.loadClass("net.corda.tools.CheckpointAgent").kotlin.companionObject
|
Class.forName("net.corda.tools.CheckpointAgent", false, javaClass.classLoader).kotlin.companionObject
|
||||||
} catch (e: ClassNotFoundException) {
|
} catch (e: ClassNotFoundException) {
|
||||||
null
|
null
|
||||||
}?.let { cls ->
|
}?.let { cls ->
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package net.corda.node.services.transactions
|
package net.corda.node.services.transactions
|
||||||
|
|
||||||
import net.corda.core.internal.BasicVerifier
|
|
||||||
import net.corda.core.internal.Verifier
|
import net.corda.core.internal.Verifier
|
||||||
import net.corda.core.serialization.ConstructorForDeserialization
|
import net.corda.core.serialization.ConstructorForDeserialization
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
@ -9,6 +8,7 @@ import net.corda.core.serialization.CordaSerializationTransformEnumDefaults
|
|||||||
import net.corda.core.serialization.CordaSerializationTransformRename
|
import net.corda.core.serialization.CordaSerializationTransformRename
|
||||||
import net.corda.core.serialization.CordaSerializationTransformRenames
|
import net.corda.core.serialization.CordaSerializationTransformRenames
|
||||||
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
|
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
|
||||||
|
import net.corda.core.serialization.SerializationContext
|
||||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||||
import net.corda.core.transactions.LedgerTransaction
|
import net.corda.core.transactions.LedgerTransaction
|
||||||
import net.corda.djvm.SandboxConfiguration
|
import net.corda.djvm.SandboxConfiguration
|
||||||
@ -80,13 +80,13 @@ class DeterministicVerifierFactoryService(
|
|||||||
override fun apply(ledgerTransaction: LedgerTransaction): LedgerTransaction {
|
override fun apply(ledgerTransaction: LedgerTransaction): LedgerTransaction {
|
||||||
// Specialise the LedgerTransaction here so that
|
// Specialise the LedgerTransaction here so that
|
||||||
// contracts are verified inside the DJVM!
|
// contracts are verified inside the DJVM!
|
||||||
return ledgerTransaction.specialise(::specialise)
|
return ledgerTransaction.specialise(::createDeterministicVerifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun specialise(ltx: LedgerTransaction, classLoader: ClassLoader): Verifier {
|
private fun createDeterministicVerifier(ltx: LedgerTransaction, serializationContext: SerializationContext): Verifier {
|
||||||
return (classLoader as? URLClassLoader)?.run {
|
return (serializationContext.deserializationClassLoader as? URLClassLoader)?.let { classLoader ->
|
||||||
DeterministicVerifier(ltx, classLoader, createSandbox(classLoader.urLs))
|
DeterministicVerifier(ltx, classLoader, createSandbox(classLoader.urLs))
|
||||||
} ?: BasicVerifier(ltx, classLoader)
|
} ?: throw IllegalStateException("Unsupported deserialization classloader type")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createSandbox(userSource: Array<URL>): SandboxConfiguration {
|
private fun createSandbox(userSource: Array<URL>): SandboxConfiguration {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package net.corda.node.internal
|
package net.corda.node.internal
|
||||||
|
|
||||||
import com.nhaarman.mockito_kotlin.whenever
|
|
||||||
import net.corda.core.serialization.CustomSerializationScheme
|
import net.corda.core.serialization.CustomSerializationScheme
|
||||||
import net.corda.core.serialization.SerializationContext
|
import net.corda.core.serialization.SerializationContext
|
||||||
import net.corda.core.serialization.SerializationSchemeContext
|
import net.corda.core.serialization.SerializationSchemeContext
|
||||||
@ -16,7 +15,7 @@ class CustomSerializationSchemeScanningTest {
|
|||||||
|
|
||||||
open class DummySerializationScheme : CustomSerializationScheme {
|
open class DummySerializationScheme : CustomSerializationScheme {
|
||||||
override fun getSchemeId(): Int {
|
override fun getSchemeId(): Int {
|
||||||
return 7;
|
return 7
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun <T : Any> deserialize(bytes: ByteSequence, clazz: Class<T>, context: SerializationSchemeContext): T {
|
override fun <T : Any> deserialize(bytes: ByteSequence, clazz: Class<T>, context: SerializationSchemeContext): T {
|
||||||
@ -34,9 +33,7 @@ class CustomSerializationSchemeScanningTest {
|
|||||||
|
|
||||||
@Test(timeout = 300_000)
|
@Test(timeout = 300_000)
|
||||||
fun `Can scan for custom serialization scheme and build a serialization scheme`() {
|
fun `Can scan for custom serialization scheme and build a serialization scheme`() {
|
||||||
val classLoader = Mockito.mock(ClassLoader::class.java)
|
val scheme = scanForCustomSerializationScheme(DummySerializationScheme::class.java.name, this::class.java.classLoader)
|
||||||
whenever(classLoader.loadClass(DummySerializationScheme::class.java.canonicalName)).thenAnswer { DummySerializationScheme::class.java }
|
|
||||||
val scheme = scanForCustomSerializationScheme(DummySerializationScheme::class.java.canonicalName, classLoader)
|
|
||||||
val mockContext = Mockito.mock(SerializationContext::class.java)
|
val mockContext = Mockito.mock(SerializationContext::class.java)
|
||||||
assertFailsWith<DummySerializationSchemeException>("Tried to serialize with DummySerializationScheme") {
|
assertFailsWith<DummySerializationSchemeException>("Tried to serialize with DummySerializationScheme") {
|
||||||
scheme.serialize(Any::class.java, mockContext)
|
scheme.serialize(Any::class.java, mockContext)
|
||||||
@ -45,34 +42,28 @@ class CustomSerializationSchemeScanningTest {
|
|||||||
|
|
||||||
@Test(timeout = 300_000)
|
@Test(timeout = 300_000)
|
||||||
fun `verification fails with a helpful error if the class is not found in the classloader`() {
|
fun `verification fails with a helpful error if the class is not found in the classloader`() {
|
||||||
val classLoader = Mockito.mock(ClassLoader::class.java)
|
val missingClassName = "org.testing.DoesNotExist"
|
||||||
val missingClassName = DummySerializationScheme::class.java.canonicalName
|
|
||||||
whenever(classLoader.loadClass(missingClassName)).thenAnswer { throw ClassNotFoundException()}
|
|
||||||
assertFailsWith<ConfigurationException>("$missingClassName was declared as a custom serialization scheme but could not " +
|
assertFailsWith<ConfigurationException>("$missingClassName was declared as a custom serialization scheme but could not " +
|
||||||
"be found.") {
|
"be found.") {
|
||||||
scanForCustomSerializationScheme(missingClassName, classLoader)
|
scanForCustomSerializationScheme(missingClassName, this::class.java.classLoader)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(timeout = 300_000)
|
@Test(timeout = 300_000)
|
||||||
fun `verification fails with a helpful error if the class is not a custom serialization scheme`() {
|
fun `verification fails with a helpful error if the class is not a custom serialization scheme`() {
|
||||||
val canonicalName = NonSerializationScheme::class.java.canonicalName
|
val schemeName = NonSerializationScheme::class.java.name
|
||||||
val classLoader = Mockito.mock(ClassLoader::class.java)
|
assertFailsWith<ConfigurationException>("$schemeName was declared as a custom serialization scheme but does not " +
|
||||||
whenever(classLoader.loadClass(canonicalName)).thenAnswer { NonSerializationScheme::class.java }
|
|
||||||
assertFailsWith<ConfigurationException>("$canonicalName was declared as a custom serialization scheme but does not " +
|
|
||||||
"implement CustomSerializationScheme.") {
|
"implement CustomSerializationScheme.") {
|
||||||
scanForCustomSerializationScheme(canonicalName, classLoader)
|
scanForCustomSerializationScheme(schemeName, this::class.java.classLoader)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(timeout = 300_000)
|
@Test(timeout = 300_000)
|
||||||
fun `verification fails with a helpful error if the class does not have a no arg constructor`() {
|
fun `verification fails with a helpful error if the class does not have a no arg constructor`() {
|
||||||
val classLoader = Mockito.mock(ClassLoader::class.java)
|
val schemeName = DummySerializationSchemeWithoutNoArgConstructor::class.java.name
|
||||||
val canonicalName = DummySerializationSchemeWithoutNoArgConstructor::class.java.canonicalName
|
assertFailsWith<ConfigurationException>("$schemeName was declared as a custom serialization scheme but does not " +
|
||||||
whenever(classLoader.loadClass(canonicalName)).thenAnswer { DummySerializationSchemeWithoutNoArgConstructor::class.java }
|
|
||||||
assertFailsWith<ConfigurationException>("$canonicalName was declared as a custom serialization scheme but does not " +
|
|
||||||
"have a no argument constructor.") {
|
"have a no argument constructor.") {
|
||||||
scanForCustomSerializationScheme(canonicalName, classLoader)
|
scanForCustomSerializationScheme(schemeName, this::class.java.classLoader)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -23,7 +23,10 @@ def javaHome = System.getProperty('java.home')
|
|||||||
def jarBaseName = "corda-${project.name}".toString()
|
def jarBaseName = "corda-${project.name}".toString()
|
||||||
|
|
||||||
configurations {
|
configurations {
|
||||||
deterministicLibraries.extendsFrom implementation
|
deterministicLibraries {
|
||||||
|
canBeConsumed = false
|
||||||
|
extendsFrom implementation
|
||||||
|
}
|
||||||
deterministicArtifacts.extendsFrom deterministicLibraries
|
deterministicArtifacts.extendsFrom deterministicLibraries
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,7 +58,7 @@ def originalJar = serializationJarTask.map { it.outputs.files.singleFile }
|
|||||||
|
|
||||||
def patchSerialization = tasks.register('patchSerialization', Zip) {
|
def patchSerialization = tasks.register('patchSerialization', Zip) {
|
||||||
dependsOn serializationJarTask
|
dependsOn serializationJarTask
|
||||||
destinationDirectory = file("$buildDir/source-libs")
|
destinationDirectory = layout.buildDirectory.dir('source-libs')
|
||||||
metadataCharset 'UTF-8'
|
metadataCharset 'UTF-8'
|
||||||
archiveClassifier = 'transient'
|
archiveClassifier = 'transient'
|
||||||
archiveExtension = 'jar'
|
archiveExtension = 'jar'
|
||||||
@ -157,7 +160,7 @@ def determinise = tasks.register('determinise', ProGuardTask) {
|
|||||||
def checkDeterminism = tasks.register('checkDeterminism', ProGuardTask)
|
def checkDeterminism = tasks.register('checkDeterminism', ProGuardTask)
|
||||||
|
|
||||||
def metafix = tasks.register('metafix', MetaFixerTask) {
|
def metafix = tasks.register('metafix', MetaFixerTask) {
|
||||||
outputDir file("$buildDir/libs")
|
outputDir = layout.buildDirectory.dir('libs')
|
||||||
jars determinise
|
jars determinise
|
||||||
suffix ""
|
suffix ""
|
||||||
|
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
|
||||||
import static org.gradle.api.JavaVersion.VERSION_1_8
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id 'org.jetbrains.kotlin.jvm'
|
id 'org.jetbrains.kotlin.jvm'
|
||||||
id 'net.corda.plugins.publish-utils'
|
id 'net.corda.plugins.publish-utils'
|
||||||
@ -17,8 +14,12 @@ apply from: "${rootProject.projectDir}/java8.gradle"
|
|||||||
description 'Serialization support for the DJVM'
|
description 'Serialization support for the DJVM'
|
||||||
|
|
||||||
configurations {
|
configurations {
|
||||||
sandboxTesting
|
sandboxTesting {
|
||||||
jdkRt
|
canBeConsumed = false
|
||||||
|
}
|
||||||
|
jdkRt {
|
||||||
|
canBeConsumed = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
@ -56,6 +57,11 @@ jar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.withType(Javadoc).configureEach {
|
||||||
|
// We have no public or protected Java classes to document.
|
||||||
|
enabled = false
|
||||||
|
}
|
||||||
|
|
||||||
tasks.withType(Test).configureEach {
|
tasks.withType(Test).configureEach {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
systemProperty 'deterministic-rt.path', configurations.jdkRt.asPath
|
systemProperty 'deterministic-rt.path', configurations.jdkRt.asPath
|
||||||
@ -66,7 +72,7 @@ tasks.withType(Test).configureEach {
|
|||||||
}
|
}
|
||||||
|
|
||||||
publish {
|
publish {
|
||||||
name jar.archiveBaseName.get()
|
name jar.archiveBaseName
|
||||||
}
|
}
|
||||||
|
|
||||||
idea {
|
idea {
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
package net.corda.serialization.djvm.serializers;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is deliberately written in Java so
|
||||||
|
* that it can be package private.
|
||||||
|
*/
|
||||||
|
final class CacheKey {
|
||||||
|
private final byte[] bytes;
|
||||||
|
private final int hashValue;
|
||||||
|
|
||||||
|
CacheKey(@NotNull byte[] bytes) {
|
||||||
|
this.bytes = bytes;
|
||||||
|
this.hashValue = Arrays.hashCode(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
byte[] getBytes() {
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object other) {
|
||||||
|
return (this == other)
|
||||||
|
|| (other instanceof CacheKey && Arrays.equals(bytes, ((CacheKey) other).bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return hashValue;
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,7 @@
|
|||||||
package net.corda.serialization.djvm.serializers
|
package net.corda.serialization.djvm.serializers
|
||||||
|
|
||||||
|
import net.corda.core.serialization.DESERIALIZATION_CACHE_PROPERTY
|
||||||
|
import net.corda.core.serialization.SerializationContext
|
||||||
import net.corda.djvm.rewiring.SandboxClassLoader
|
import net.corda.djvm.rewiring.SandboxClassLoader
|
||||||
import net.corda.serialization.djvm.deserializers.CertPathDeserializer
|
import net.corda.serialization.djvm.deserializers.CertPathDeserializer
|
||||||
import net.corda.serialization.djvm.toSandboxAnyClass
|
import net.corda.serialization.djvm.toSandboxAnyClass
|
||||||
@ -27,4 +29,13 @@ class SandboxCertPathSerializer(
|
|||||||
override fun fromProxy(proxy: Any): Any {
|
override fun fromProxy(proxy: Any): Any {
|
||||||
return task.apply(proxy)!!
|
return task.apply(proxy)!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun fromProxy(proxy: Any, context: SerializationContext): Any {
|
||||||
|
// This requires [CertPathProxy] to have correct
|
||||||
|
// implementations for [equals] and [hashCode].
|
||||||
|
@Suppress("unchecked_cast")
|
||||||
|
return (context.properties[DESERIALIZATION_CACHE_PROPERTY] as? MutableMap<Any, Any>)
|
||||||
|
?.computeIfAbsent(proxy, ::fromProxy)
|
||||||
|
?: fromProxy(proxy)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package net.corda.serialization.djvm.serializers
|
package net.corda.serialization.djvm.serializers
|
||||||
|
|
||||||
|
import net.corda.core.serialization.DESERIALIZATION_CACHE_PROPERTY
|
||||||
import net.corda.core.serialization.SerializationContext
|
import net.corda.core.serialization.SerializationContext
|
||||||
import net.corda.djvm.rewiring.SandboxClassLoader
|
import net.corda.djvm.rewiring.SandboxClassLoader
|
||||||
import net.corda.serialization.djvm.deserializers.PublicKeyDecoder
|
import net.corda.serialization.djvm.deserializers.PublicKeyDecoder
|
||||||
@ -27,7 +28,11 @@ class SandboxPublicKeySerializer(
|
|||||||
|
|
||||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any {
|
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any {
|
||||||
val bits = input.readObject(obj, schemas, ByteArray::class.java, context) as ByteArray
|
val bits = input.readObject(obj, schemas, ByteArray::class.java, context) as ByteArray
|
||||||
return decoder.apply(bits)!!
|
@Suppress("unchecked_cast")
|
||||||
|
return (context.properties[DESERIALIZATION_CACHE_PROPERTY] as? MutableMap<CacheKey, Any?>)
|
||||||
|
?.computeIfAbsent(CacheKey(bits)) { key ->
|
||||||
|
decoder.apply(key.bytes)
|
||||||
|
} ?: decoder.apply(bits)!!
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun writeDescribedObject(obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext) {
|
override fun writeDescribedObject(obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext) {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package net.corda.serialization.djvm.serializers
|
package net.corda.serialization.djvm.serializers
|
||||||
|
|
||||||
|
import net.corda.core.serialization.DESERIALIZATION_CACHE_PROPERTY
|
||||||
import net.corda.core.serialization.SerializationContext
|
import net.corda.core.serialization.SerializationContext
|
||||||
import net.corda.djvm.rewiring.SandboxClassLoader
|
import net.corda.djvm.rewiring.SandboxClassLoader
|
||||||
import net.corda.serialization.djvm.deserializers.X509CRLDeserializer
|
import net.corda.serialization.djvm.deserializers.X509CRLDeserializer
|
||||||
@ -28,7 +29,11 @@ class SandboxX509CRLSerializer(
|
|||||||
|
|
||||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any {
|
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any {
|
||||||
val bits = input.readObject(obj, schemas, ByteArray::class.java, context) as ByteArray
|
val bits = input.readObject(obj, schemas, ByteArray::class.java, context) as ByteArray
|
||||||
return generator.apply(bits)!!
|
@Suppress("unchecked_cast")
|
||||||
|
return (context.properties[DESERIALIZATION_CACHE_PROPERTY] as? MutableMap<CacheKey, Any?>)
|
||||||
|
?.computeIfAbsent(CacheKey(bits)) { key ->
|
||||||
|
generator.apply(key.bytes)
|
||||||
|
} ?: generator.apply(bits)!!
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun writeDescribedObject(obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext) {
|
override fun writeDescribedObject(obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext) {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package net.corda.serialization.djvm.serializers
|
package net.corda.serialization.djvm.serializers
|
||||||
|
|
||||||
|
import net.corda.core.serialization.DESERIALIZATION_CACHE_PROPERTY
|
||||||
import net.corda.core.serialization.SerializationContext
|
import net.corda.core.serialization.SerializationContext
|
||||||
import net.corda.djvm.rewiring.SandboxClassLoader
|
import net.corda.djvm.rewiring.SandboxClassLoader
|
||||||
import net.corda.serialization.djvm.deserializers.X509CertificateDeserializer
|
import net.corda.serialization.djvm.deserializers.X509CertificateDeserializer
|
||||||
@ -28,7 +29,11 @@ class SandboxX509CertificateSerializer(
|
|||||||
|
|
||||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any {
|
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any {
|
||||||
val bits = input.readObject(obj, schemas, ByteArray::class.java, context) as ByteArray
|
val bits = input.readObject(obj, schemas, ByteArray::class.java, context) as ByteArray
|
||||||
return generator.apply(bits)!!
|
@Suppress("unchecked_cast")
|
||||||
|
return (context.properties[DESERIALIZATION_CACHE_PROPERTY] as? MutableMap<CacheKey, Any?>)
|
||||||
|
?.computeIfAbsent(CacheKey(bits)) { key ->
|
||||||
|
generator.apply(key.bytes)
|
||||||
|
} ?: generator.apply(bits)!!
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun writeDescribedObject(obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext) {
|
override fun writeDescribedObject(obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext) {
|
||||||
|
@ -52,8 +52,13 @@ configurations {
|
|||||||
testArtifacts.extendsFrom testRuntimeClasspath
|
testArtifacts.extendsFrom testRuntimeClasspath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.withType(Javadoc).configureEach {
|
||||||
|
// We have no public or protected Java classes to document.
|
||||||
|
enabled = false
|
||||||
|
}
|
||||||
|
|
||||||
task testJar(type: Jar) {
|
task testJar(type: Jar) {
|
||||||
classifier "tests"
|
archiveClassifier = 'tests'
|
||||||
from sourceSets.test.output
|
from sourceSets.test.output
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,5 +73,5 @@ jar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
publish {
|
publish {
|
||||||
name jar.baseName
|
name jar.archiveBaseName
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
package net.corda.serialization.internal.amqp.custom;
|
||||||
|
|
||||||
|
import net.corda.core.KeepForDJVM;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is deliberately written in Java so
|
||||||
|
* that it can be package private.
|
||||||
|
*/
|
||||||
|
@KeepForDJVM
|
||||||
|
final class CacheKey {
|
||||||
|
private final byte[] bytes;
|
||||||
|
private final int hashValue;
|
||||||
|
|
||||||
|
CacheKey(@NotNull byte[] bytes) {
|
||||||
|
this.bytes = bytes;
|
||||||
|
this.hashValue = Arrays.hashCode(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
byte[] getBytes() {
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object other) {
|
||||||
|
return (this == other)
|
||||||
|
|| (other instanceof CacheKey && Arrays.equals(bytes, ((CacheKey) other).bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return hashValue;
|
||||||
|
}
|
||||||
|
}
|
@ -40,6 +40,7 @@ data class SerializationContextImpl @JvmOverloads constructor(override val prefe
|
|||||||
/**
|
/**
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
*/
|
*/
|
||||||
|
@Suppress("OverridingDeprecatedMember")
|
||||||
override fun withAttachmentsClassLoader(attachmentHashes: List<SecureHash>): SerializationContext {
|
override fun withAttachmentsClassLoader(attachmentHashes: List<SecureHash>): SerializationContext {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
@ -108,12 +109,13 @@ open class SerializationFactoryImpl(
|
|||||||
val lookupKey = magic to target
|
val lookupKey = magic to target
|
||||||
// ConcurrentHashMap.get() is lock free, but computeIfAbsent is not, even if the key is in the map already.
|
// ConcurrentHashMap.get() is lock free, but computeIfAbsent is not, even if the key is in the map already.
|
||||||
return (schemes[lookupKey] ?: schemes.computeIfAbsent(lookupKey) {
|
return (schemes[lookupKey] ?: schemes.computeIfAbsent(lookupKey) {
|
||||||
registeredSchemes.filter { it.canDeserializeVersion(magic, target) }.forEach { return@computeIfAbsent it } // XXX: Not single?
|
registeredSchemes.firstOrNull { it.canDeserializeVersion(magic, target) } ?: run {
|
||||||
logger.warn("Cannot find serialization scheme for: [$lookupKey, " +
|
logger.warn("Cannot find serialization scheme for: [$lookupKey, " +
|
||||||
"${if (magic == amqpMagic) "AMQP" else "UNKNOWN MAGIC"}] registeredSchemes are: $registeredSchemes")
|
"${if (magic == amqpMagic) "AMQP" else "UNKNOWN MAGIC"}] registeredSchemes are: $registeredSchemes")
|
||||||
val schemeId = getSchemeIdIfCustomSerializationMagic(magic) ?: throw UnsupportedOperationException("Serialization scheme" +
|
val schemeId = getSchemeIdIfCustomSerializationMagic(magic) ?: throw UnsupportedOperationException("Serialization scheme" +
|
||||||
" $lookupKey not supported.")
|
" $lookupKey not supported.")
|
||||||
throw UnsupportedOperationException("Could not find custom serialization scheme with SchemeId = $schemeId.")
|
throw UnsupportedOperationException("Could not find custom serialization scheme with SchemeId = $schemeId.")
|
||||||
|
}
|
||||||
}) to magic
|
}) to magic
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,11 +88,11 @@ class CorDappCustomSerializer(
|
|||||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput,
|
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput,
|
||||||
context: SerializationContext
|
context: SerializationContext
|
||||||
) = uncheckedCast<SerializationCustomSerializer<*, *>, SerializationCustomSerializer<Any?, Any?>>(
|
) = uncheckedCast<SerializationCustomSerializer<*, *>, SerializationCustomSerializer<Any?, Any?>>(
|
||||||
serializer).fromProxy(uncheckedCast(proxySerializer.readObject(obj, schemas, input, context)))!!
|
serializer).fromProxy(proxySerializer.readObject(obj, schemas, input, context))!!
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For 3rd party plugin serializers we are going to exist on exact type matching. i.e. we will
|
* For 3rd party plugin serializers we are going to exist on exact type matching. i.e. we will
|
||||||
* not support base class serializers for derivedtypes
|
* not support base class serializers for derived types
|
||||||
*/
|
*/
|
||||||
override fun isSerializerFor(clazz: Class<*>) =
|
override fun isSerializerFor(clazz: Class<*>) =
|
||||||
TypeToken.of(type.asClass()) == TypeToken.of(clazz)
|
TypeToken.of(type.asClass()) == TypeToken.of(clazz)
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package net.corda.serialization.internal.amqp
|
package net.corda.serialization.internal.amqp
|
||||||
|
|
||||||
import net.corda.core.KeepForDJVM
|
import net.corda.core.KeepForDJVM
|
||||||
import net.corda.core.internal.uncheckedCast
|
|
||||||
import net.corda.core.serialization.SerializationContext
|
import net.corda.core.serialization.SerializationContext
|
||||||
import net.corda.serialization.internal.model.FingerprintWriter
|
import net.corda.serialization.internal.model.FingerprintWriter
|
||||||
import net.corda.serialization.internal.model.TypeIdentifier
|
import net.corda.serialization.internal.model.TypeIdentifier
|
||||||
@ -52,7 +51,8 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T>, SerializerFor {
|
|||||||
context: SerializationContext, debugIndent: Int
|
context: SerializationContext, debugIndent: Int
|
||||||
) {
|
) {
|
||||||
data.withDescribed(descriptor) {
|
data.withDescribed(descriptor) {
|
||||||
writeDescribedObject(uncheckedCast(obj), data, type, output, context)
|
@Suppress("unchecked_cast")
|
||||||
|
writeDescribedObject(obj as T, data, type, output, context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,10 +178,13 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T>, SerializerFor {
|
|||||||
|
|
||||||
protected abstract fun fromProxy(proxy: P): T
|
protected abstract fun fromProxy(proxy: P): T
|
||||||
|
|
||||||
|
protected open fun toProxy(obj: T, context: SerializationContext): P = toProxy(obj)
|
||||||
|
protected open fun fromProxy(proxy: P, context: SerializationContext): T = fromProxy(proxy)
|
||||||
|
|
||||||
override fun writeDescribedObject(obj: T, data: Data, type: Type, output: SerializationOutput,
|
override fun writeDescribedObject(obj: T, data: Data, type: Type, output: SerializationOutput,
|
||||||
context: SerializationContext
|
context: SerializationContext
|
||||||
) {
|
) {
|
||||||
val proxy = toProxy(obj)
|
val proxy = toProxy(obj, context)
|
||||||
data.withList {
|
data.withList {
|
||||||
proxySerializer.propertySerializers.forEach { (_, serializer) ->
|
proxySerializer.propertySerializers.forEach { (_, serializer) ->
|
||||||
serializer.writeProperty(proxy, this, output, context, 0)
|
serializer.writeProperty(proxy, this, output, context, 0)
|
||||||
@ -192,8 +195,9 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T>, SerializerFor {
|
|||||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput,
|
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput,
|
||||||
context: SerializationContext
|
context: SerializationContext
|
||||||
): T {
|
): T {
|
||||||
val proxy: P = uncheckedCast(proxySerializer.readObject(obj, schemas, input, context))
|
@Suppress("unchecked_cast")
|
||||||
return fromProxy(proxy)
|
val proxy = proxySerializer.readObject(obj, schemas, input, context) as P
|
||||||
|
return fromProxy(proxy, context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,12 +3,17 @@ package net.corda.serialization.internal.amqp
|
|||||||
import net.corda.core.KeepForDJVM
|
import net.corda.core.KeepForDJVM
|
||||||
import net.corda.core.internal.VisibleForTesting
|
import net.corda.core.internal.VisibleForTesting
|
||||||
import net.corda.core.serialization.EncodingWhitelist
|
import net.corda.core.serialization.EncodingWhitelist
|
||||||
|
import net.corda.core.serialization.AMQP_ENVELOPE_CACHE_PROPERTY
|
||||||
import net.corda.core.serialization.SerializationContext
|
import net.corda.core.serialization.SerializationContext
|
||||||
import net.corda.core.serialization.SerializedBytes
|
import net.corda.core.serialization.SerializedBytes
|
||||||
import net.corda.core.utilities.ByteSequence
|
import net.corda.core.utilities.ByteSequence
|
||||||
import net.corda.core.utilities.loggerFor
|
import net.corda.core.utilities.loggerFor
|
||||||
import net.corda.core.utilities.trace
|
import net.corda.core.utilities.trace
|
||||||
import net.corda.serialization.internal.*
|
import net.corda.serialization.internal.ByteBufferInputStream
|
||||||
|
import net.corda.serialization.internal.CordaSerializationEncoding
|
||||||
|
import net.corda.serialization.internal.NullEncodingWhitelist
|
||||||
|
import net.corda.serialization.internal.SectionId
|
||||||
|
import net.corda.serialization.internal.encodingNotPermittedFormat
|
||||||
import net.corda.serialization.internal.model.TypeIdentifier
|
import net.corda.serialization.internal.model.TypeIdentifier
|
||||||
import org.apache.qpid.proton.amqp.Binary
|
import org.apache.qpid.proton.amqp.Binary
|
||||||
import org.apache.qpid.proton.amqp.DescribedType
|
import org.apache.qpid.proton.amqp.DescribedType
|
||||||
@ -118,7 +123,19 @@ class DeserializationInput constructor(
|
|||||||
@Throws(NotSerializableException::class)
|
@Throws(NotSerializableException::class)
|
||||||
fun <T : Any> deserialize(bytes: ByteSequence, clazz: Class<T>, context: SerializationContext): T =
|
fun <T : Any> deserialize(bytes: ByteSequence, clazz: Class<T>, context: SerializationContext): T =
|
||||||
des {
|
des {
|
||||||
val envelope = getEnvelope(bytes, context.encodingWhitelist)
|
/**
|
||||||
|
* The cache uses object identity rather than [ByteSequence.equals] and
|
||||||
|
* [ByteSequence.hashCode]. This is for speed: each [ByteSequence] object
|
||||||
|
* can potentially be large, and we are optimizing for the case when we
|
||||||
|
* know we will be deserializing the exact same objects multiple times.
|
||||||
|
* This also means that the cache MUST be short-lived, as otherwise it
|
||||||
|
* becomes a memory leak.
|
||||||
|
*/
|
||||||
|
@Suppress("unchecked_cast")
|
||||||
|
val envelope = (context.properties[AMQP_ENVELOPE_CACHE_PROPERTY] as? MutableMap<IdentityKey, Envelope>)
|
||||||
|
?.computeIfAbsent(IdentityKey(bytes)) { key ->
|
||||||
|
getEnvelope(key.bytes, context.encodingWhitelist)
|
||||||
|
} ?: getEnvelope(bytes, context.encodingWhitelist)
|
||||||
|
|
||||||
logger.trace { "deserialize blob scheme=\"${envelope.schema}\"" }
|
logger.trace { "deserialize blob scheme=\"${envelope.schema}\"" }
|
||||||
|
|
||||||
@ -219,3 +236,16 @@ class DeserializationInput constructor(
|
|||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We cannot use [ByteSequence.equals] and [ByteSequence.hashCode] because
|
||||||
|
* these consider the contents of the underlying [ByteArray] object. We
|
||||||
|
* only need the [ByteSequence]'s object identity for our use-case.
|
||||||
|
*/
|
||||||
|
private class IdentityKey(val bytes: ByteSequence) {
|
||||||
|
override fun hashCode() = System.identityHashCode(bytes)
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
return (this === other) || (other is IdentityKey && bytes === other.bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package net.corda.serialization.internal.amqp.custom
|
package net.corda.serialization.internal.amqp.custom
|
||||||
|
|
||||||
import net.corda.core.KeepForDJVM
|
import net.corda.core.KeepForDJVM
|
||||||
|
import net.corda.core.serialization.DESERIALIZATION_CACHE_PROPERTY
|
||||||
|
import net.corda.core.serialization.SerializationContext
|
||||||
import net.corda.serialization.internal.amqp.CustomSerializer
|
import net.corda.serialization.internal.amqp.CustomSerializer
|
||||||
import net.corda.serialization.internal.amqp.SerializerFactory
|
import net.corda.serialization.internal.amqp.SerializerFactory
|
||||||
import java.io.NotSerializableException
|
import java.io.NotSerializableException
|
||||||
@ -28,7 +30,21 @@ class CertPathSerializer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun fromProxy(proxy: CertPathProxy, context: SerializationContext): CertPath {
|
||||||
|
// This requires [CertPathProxy] to have correct
|
||||||
|
// implementations for [equals] and [hashCode].
|
||||||
|
@Suppress("unchecked_cast")
|
||||||
|
return (context.properties[DESERIALIZATION_CACHE_PROPERTY] as? MutableMap<CertPathProxy, CertPath>)
|
||||||
|
?.computeIfAbsent(proxy, ::fromProxy)
|
||||||
|
?: fromProxy(proxy)
|
||||||
|
}
|
||||||
|
|
||||||
@KeepForDJVM
|
@KeepForDJVM
|
||||||
@Suppress("ArrayInDataClass")
|
data class CertPathProxy(val type: String, val encoded: ByteArray) {
|
||||||
data class CertPathProxy(val type: String, val encoded: ByteArray)
|
override fun hashCode() = (type.hashCode() * 31) + encoded.contentHashCode()
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
return (this === other)
|
||||||
|
|| (other is CertPathProxy && (type == other.type && encoded.contentEquals(other.encoded)))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,6 +1,7 @@
|
|||||||
package net.corda.serialization.internal.amqp.custom
|
package net.corda.serialization.internal.amqp.custom
|
||||||
|
|
||||||
import net.corda.core.crypto.Crypto
|
import net.corda.core.crypto.Crypto
|
||||||
|
import net.corda.core.serialization.DESERIALIZATION_CACHE_PROPERTY
|
||||||
import net.corda.core.serialization.SerializationContext
|
import net.corda.core.serialization.SerializationContext
|
||||||
import net.corda.serialization.internal.amqp.*
|
import net.corda.serialization.internal.amqp.*
|
||||||
import org.apache.qpid.proton.codec.Data
|
import org.apache.qpid.proton.codec.Data
|
||||||
@ -34,6 +35,10 @@ object PublicKeySerializer
|
|||||||
context: SerializationContext
|
context: SerializationContext
|
||||||
): PublicKey {
|
): PublicKey {
|
||||||
val bits = input.readObject(obj, schemas, ByteArray::class.java, context) as ByteArray
|
val bits = input.readObject(obj, schemas, ByteArray::class.java, context) as ByteArray
|
||||||
return Crypto.decodePublicKey(bits)
|
@Suppress("unchecked_cast")
|
||||||
|
return (context.properties[DESERIALIZATION_CACHE_PROPERTY] as? MutableMap<CacheKey, PublicKey>)
|
||||||
|
?.computeIfAbsent(CacheKey(bits)) { key ->
|
||||||
|
Crypto.decodePublicKey(key.bytes)
|
||||||
|
} ?: Crypto.decodePublicKey(bits)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,5 +1,6 @@
|
|||||||
package net.corda.serialization.internal.amqp.custom
|
package net.corda.serialization.internal.amqp.custom
|
||||||
|
|
||||||
|
import net.corda.core.serialization.DESERIALIZATION_CACHE_PROPERTY
|
||||||
import net.corda.core.serialization.SerializationContext
|
import net.corda.core.serialization.SerializationContext
|
||||||
import net.corda.serialization.internal.amqp.*
|
import net.corda.serialization.internal.amqp.*
|
||||||
import org.apache.qpid.proton.codec.Data
|
import org.apache.qpid.proton.codec.Data
|
||||||
@ -28,6 +29,14 @@ object X509CRLSerializer
|
|||||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput,
|
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput,
|
||||||
context: SerializationContext): X509CRL {
|
context: SerializationContext): X509CRL {
|
||||||
val bytes = input.readObject(obj, schemas, ByteArray::class.java, context) as ByteArray
|
val bytes = input.readObject(obj, schemas, ByteArray::class.java, context) as ByteArray
|
||||||
|
@Suppress("unchecked_cast")
|
||||||
|
return (context.properties[DESERIALIZATION_CACHE_PROPERTY] as? MutableMap<CacheKey, X509CRL>)
|
||||||
|
?.computeIfAbsent(CacheKey(bytes)) { key ->
|
||||||
|
generateCRL(key.bytes)
|
||||||
|
} ?: generateCRL(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateCRL(bytes: ByteArray): X509CRL {
|
||||||
return CertificateFactory.getInstance("X.509").generateCRL(bytes.inputStream()) as X509CRL
|
return CertificateFactory.getInstance("X.509").generateCRL(bytes.inputStream()) as X509CRL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package net.corda.serialization.internal.amqp.custom
|
package net.corda.serialization.internal.amqp.custom
|
||||||
|
|
||||||
|
import net.corda.core.serialization.DESERIALIZATION_CACHE_PROPERTY
|
||||||
import net.corda.core.serialization.SerializationContext
|
import net.corda.core.serialization.SerializationContext
|
||||||
import net.corda.serialization.internal.amqp.*
|
import net.corda.serialization.internal.amqp.*
|
||||||
import org.apache.qpid.proton.codec.Data
|
import org.apache.qpid.proton.codec.Data
|
||||||
@ -28,6 +29,14 @@ object X509CertificateSerializer
|
|||||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput,
|
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput,
|
||||||
context: SerializationContext): X509Certificate {
|
context: SerializationContext): X509Certificate {
|
||||||
val bits = input.readObject(obj, schemas, ByteArray::class.java, context) as ByteArray
|
val bits = input.readObject(obj, schemas, ByteArray::class.java, context) as ByteArray
|
||||||
|
@Suppress("unchecked_cast")
|
||||||
|
return (context.properties[DESERIALIZATION_CACHE_PROPERTY] as? MutableMap<CacheKey, X509Certificate>)
|
||||||
|
?.computeIfAbsent(CacheKey(bits)) { key ->
|
||||||
|
generateCertificate(key.bytes)
|
||||||
|
} ?: generateCertificate(bits)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateCertificate(bits: ByteArray): X509Certificate {
|
||||||
return CertificateFactory.getInstance("X.509").generateCertificate(bits.inputStream()) as X509Certificate
|
return CertificateFactory.getInstance("X.509").generateCertificate(bits.inputStream()) as X509Certificate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,6 @@ import org.objectweb.asm.Type
|
|||||||
import java.lang.Character.isJavaIdentifierPart
|
import java.lang.Character.isJavaIdentifierPart
|
||||||
import java.lang.Character.isJavaIdentifierStart
|
import java.lang.Character.isJavaIdentifierStart
|
||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Any object that implements this interface is expected to expose its own fields via the [get] method, exactly
|
* Any object that implements this interface is expected to expose its own fields via the [get] method, exactly
|
||||||
@ -28,8 +27,23 @@ interface SimpleFieldAccess {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@DeleteForDJVM
|
@DeleteForDJVM
|
||||||
class CarpenterClassLoader(parentClassLoader: ClassLoader = Thread.currentThread().contextClassLoader) :
|
class CarpenterClassLoader(private val parentClassLoader: ClassLoader = Thread.currentThread().contextClassLoader) :
|
||||||
ClassLoader(parentClassLoader) {
|
ClassLoader(parentClassLoader) {
|
||||||
|
@Throws(ClassNotFoundException::class)
|
||||||
|
override fun loadClass(name: String?, resolve: Boolean): Class<*>? {
|
||||||
|
return synchronized(getClassLoadingLock(name)) {
|
||||||
|
/**
|
||||||
|
* Search parent classloaders using lock-less [Class.forName],
|
||||||
|
* bypassing [parent] to avoid its [SecurityManager] overhead.
|
||||||
|
*/
|
||||||
|
(findLoadedClass(name) ?: Class.forName(name, false, parentClassLoader)).also { clazz ->
|
||||||
|
if (resolve) {
|
||||||
|
resolveClass(clazz)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun load(name: String, bytes: ByteArray): Class<*> {
|
fun load(name: String, bytes: ByteArray): Class<*> {
|
||||||
return defineClass(name, bytes, 0, bytes.size)
|
return defineClass(name, bytes, 0, bytes.size)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user