mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
CORDA-2157 - Hash to Signature constraints migration V2 (#4261)
* Hash to signature constraints migration #1 * After rebase from Attachments Classloader commit. * Simplified implementation without CZ whitelisting and relaxing the no-overlap rule slightly. * Further simplification. * Detailed implementation. * Use fully loaded Cash contract jar for hash to signature constraints migration test. Additional debug logging. * Minor cleanup. * Address PR review feedback. * Minor fix. * Fixes following rebase from master. * Implemented `calculateEntriesHashes` to improve classloader validation performance. * Address minor PR review comments. * Added integration tests and some minor fixes. * Minor fixes following rebase from master. * Updates and fixes following integration testing. * Added changelog entry. * Fix broken unit tests. * Fix compilation errors in DriverDSL tests after rebase from master. * Minor fix to test cordapp jar signing using explicit keystore. * Run hash-to-signature constraints integration test out of process using a non-validating notary. * Address PR review feedback: contract version from database + other minor changes. * Address final PR review feedback: remove signed attachment field from attachmentWithContext * Resolve conflicts following rebase from master. * Fix failing junit test. * Fix Kryo serialization error (forgot to write new `version` identifier field) * Removed redundant query carried over from previous commit. * Added documentation. * Fix test case where explicit Hash Constraint input and Signature Constraint output explicitly configured. * Addressing PR review comments from SA. * AttachmentQueryCriteria API: added wither methods and Java Unit tests. * Fixed compilation error caused by Unit tests being in wrong module. * Added @CordaInternal to canBeTransitionedFrom function. * Minimized AttachmentClassloader overlap duplicates checking. * Moved JarSignatureTestUtils and ContractJarTestUtils to internal pending clean-up and documentation before public release. * Minor fix following rebase from master. * Removed redundant checkNotNull(networkParameters) checks now that these are always passed into the main (non-deprecated) constructor. * Remove capitalization.
This commit is contained in:
parent
382e3b651f
commit
63e326aedb
@ -3463,22 +3463,45 @@ public static final class net.corda.core.node.services.vault.AttachmentQueryCrit
|
||||
public <init>(net.corda.core.node.services.vault.ColumnPredicate<String>)
|
||||
public <init>(net.corda.core.node.services.vault.ColumnPredicate<String>, net.corda.core.node.services.vault.ColumnPredicate<String>)
|
||||
public <init>(net.corda.core.node.services.vault.ColumnPredicate<String>, net.corda.core.node.services.vault.ColumnPredicate<String>, net.corda.core.node.services.vault.ColumnPredicate<java.time.Instant>)
|
||||
public <init>(net.corda.core.node.services.vault.ColumnPredicate<String>, net.corda.core.node.services.vault.ColumnPredicate<String>, net.corda.core.node.services.vault.ColumnPredicate<java.time.Instant>, net.corda.core.node.services.vault.ColumnPredicate<java.util.List<String>>)
|
||||
public <init>(net.corda.core.node.services.vault.ColumnPredicate<String>, net.corda.core.node.services.vault.ColumnPredicate<String>, net.corda.core.node.services.vault.ColumnPredicate<java.time.Instant>, net.corda.core.node.services.vault.ColumnPredicate<java.util.List<String>>, net.corda.core.node.services.vault.ColumnPredicate<java.util.List<java.security.PublicKey>>)
|
||||
public <init>(net.corda.core.node.services.vault.ColumnPredicate<String>, net.corda.core.node.services.vault.ColumnPredicate<String>, net.corda.core.node.services.vault.ColumnPredicate<java.time.Instant>, net.corda.core.node.services.vault.ColumnPredicate<java.util.List<String>>, net.corda.core.node.services.vault.ColumnPredicate<java.util.List<java.security.PublicKey>>, net.corda.core.node.services.vault.ColumnPredicate<Boolean>)
|
||||
public <init>(net.corda.core.node.services.vault.ColumnPredicate<String>, net.corda.core.node.services.vault.ColumnPredicate<String>, net.corda.core.node.services.vault.ColumnPredicate<java.time.Instant>, net.corda.core.node.services.vault.ColumnPredicate<java.util.List<String>>, net.corda.core.node.services.vault.ColumnPredicate<java.util.List<java.security.PublicKey>>, net.corda.core.node.services.vault.ColumnPredicate<Boolean>, net.corda.core.node.services.vault.ColumnPredicate<java.util.List<String>>)
|
||||
@Nullable
|
||||
public final net.corda.core.node.services.vault.ColumnPredicate<String> component1()
|
||||
@Nullable
|
||||
public final net.corda.core.node.services.vault.ColumnPredicate<String> component2()
|
||||
@Nullable
|
||||
public final net.corda.core.node.services.vault.ColumnPredicate<java.time.Instant> component3()
|
||||
@Nullable
|
||||
public final net.corda.core.node.services.vault.ColumnPredicate<java.util.List<String>> component4()
|
||||
@Nullable
|
||||
public final net.corda.core.node.services.vault.ColumnPredicate<java.util.List<java.security.PublicKey>> component5()
|
||||
@Nullable
|
||||
public final net.corda.core.node.services.vault.ColumnPredicate<Boolean> component6()
|
||||
@Nullable
|
||||
public final net.corda.core.node.services.vault.ColumnPredicate<java.util.List<String>> component7()
|
||||
@NotNull
|
||||
public final net.corda.core.node.services.vault.AttachmentQueryCriteria$AttachmentsQueryCriteria copy(net.corda.core.node.services.vault.ColumnPredicate<String>, net.corda.core.node.services.vault.ColumnPredicate<String>, net.corda.core.node.services.vault.ColumnPredicate<java.time.Instant>)
|
||||
@NotNull
|
||||
public final net.corda.core.node.services.vault.AttachmentQueryCriteria$AttachmentsQueryCriteria copy(net.corda.core.node.services.vault.ColumnPredicate<String>, net.corda.core.node.services.vault.ColumnPredicate<String>, net.corda.core.node.services.vault.ColumnPredicate<java.time.Instant>, net.corda.core.node.services.vault.ColumnPredicate<java.util.List<String>>, net.corda.core.node.services.vault.ColumnPredicate<java.util.List<java.security.PublicKey>>, net.corda.core.node.services.vault.ColumnPredicate<Boolean>, net.corda.core.node.services.vault.ColumnPredicate<java.util.List<String>>)
|
||||
public boolean equals(Object)
|
||||
@Nullable
|
||||
public final net.corda.core.node.services.vault.ColumnPredicate<java.util.List<String>> getContractClassNamesCondition()
|
||||
@Nullable
|
||||
public final net.corda.core.node.services.vault.ColumnPredicate<String> getFilenameCondition()
|
||||
@Nullable
|
||||
public final net.corda.core.node.services.vault.ColumnPredicate<java.util.List<java.security.PublicKey>> getSignersCondition()
|
||||
@Nullable
|
||||
public final net.corda.core.node.services.vault.ColumnPredicate<java.time.Instant> getUploadDateCondition()
|
||||
@Nullable
|
||||
public final net.corda.core.node.services.vault.ColumnPredicate<String> getUploaderCondition()
|
||||
@Nullable
|
||||
public final net.corda.core.node.services.vault.ColumnPredicate<java.util.List<String>> getVersionCondition()
|
||||
public int hashCode()
|
||||
@Nullable
|
||||
public final net.corda.core.node.services.vault.ColumnPredicate<Boolean> isSignedCondition()
|
||||
@NotNull
|
||||
public String toString()
|
||||
@NotNull
|
||||
public java.util.Collection<javax.persistence.criteria.Predicate> visit(net.corda.core.node.services.vault.AttachmentsQueryCriteriaParser)
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.CordaInternal
|
||||
import net.corda.core.DoNotImplement
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint.isSatisfiedBy
|
||||
@ -8,7 +9,9 @@ import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.crypto.keys
|
||||
import net.corda.core.internal.AttachmentWithContext
|
||||
import net.corda.core.internal.isUploaderTrusted
|
||||
import net.corda.core.serialization.internal.AttachmentsClassLoader
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.warnOnce
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.lang.annotation.Inherited
|
||||
@ -32,6 +35,10 @@ interface AttachmentConstraint {
|
||||
/** Returns whether the given contract attachment can be used with the [ContractState] associated with this constraint object. */
|
||||
fun isSatisfiedBy(attachment: Attachment): Boolean
|
||||
|
||||
private companion object {
|
||||
private val log = contextLogger()
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will be used in conjunction with [NoConstraintPropagation]. It is run during transaction verification when the contract is not annotated with [NoConstraintPropagation].
|
||||
* When constraints propagation is enabled, constraints set on output states need to follow certain rules with regards to constraints of input states.
|
||||
@ -39,13 +46,17 @@ interface AttachmentConstraint {
|
||||
* Rules:
|
||||
* * It is allowed for output states to inherit the exact same constraint as the input states.
|
||||
* * The [AlwaysAcceptAttachmentConstraint] is not allowed to transition to a different constraint, as that could be used to hide malicious behaviour.
|
||||
* * Nothing can be migrated from the [HashAttachmentConstraint] except a [HashAttachmentConstraint] with the same hash.
|
||||
* * Anything (except the [AlwaysAcceptAttachmentConstraint]) can be transitioned to a [HashAttachmentConstraint].
|
||||
* * You can transition from the [WhitelistedByZoneAttachmentConstraint] to the [SignatureAttachmentConstraint] only if all signers of the JAR are required to sign in the future.
|
||||
* * You can transition from a [HashAttachmentConstraint] to a [SignatureAttachmentConstraint] when the following conditions are met:
|
||||
* * 1. Jar contents (per entry, by hashcode) of both original (unsigned) and signed contract jars are identical
|
||||
* * Note: this step is enforced in the [AttachmentsClassLoader] no overlap rule checking.
|
||||
* * 2. Java package namespace of signed contract jar is registered in the CZ network map with same public keys (as used to sign contract jar)
|
||||
*
|
||||
* TODO - SignatureConstraint third party signers.
|
||||
*/
|
||||
fun canBeTransitionedFrom(input: AttachmentConstraint, attachment: ContractAttachment): Boolean {
|
||||
@CordaInternal
|
||||
fun canBeTransitionedFrom(input: AttachmentConstraint, attachment: AttachmentWithContext): Boolean {
|
||||
val output = this
|
||||
return when {
|
||||
// These branches should not happen, as this has been already checked.
|
||||
@ -59,9 +70,7 @@ interface AttachmentConstraint {
|
||||
input is AlwaysAcceptAttachmentConstraint && output !is AlwaysAcceptAttachmentConstraint -> false
|
||||
|
||||
// Nothing can be migrated from the HashConstraint except a HashConstraint with the same Hash. (This check is redundant, but added for clarity)
|
||||
// TODO - this might change if we decide to allow migration to the SignatureConstraint.
|
||||
input is HashAttachmentConstraint && output is HashAttachmentConstraint -> input == output
|
||||
input is HashAttachmentConstraint && output !is HashAttachmentConstraint -> false
|
||||
|
||||
// Anything (except the AlwaysAcceptAttachmentConstraint) can be transformed to a HashAttachmentConstraint.
|
||||
input !is HashAttachmentConstraint && output is HashAttachmentConstraint -> true
|
||||
@ -74,6 +83,20 @@ interface AttachmentConstraint {
|
||||
input is WhitelistedByZoneAttachmentConstraint && output is SignatureAttachmentConstraint ->
|
||||
attachment.signerKeys.isNotEmpty() && output.key.keys.containsAll(attachment.signerKeys)
|
||||
|
||||
// Transition from Hash to Signature constraint requires
|
||||
// signer(s) of signature-constrained output state is same as signer(s) of registered package namespace
|
||||
input is HashAttachmentConstraint && output is SignatureAttachmentConstraint -> {
|
||||
val packageOwnerPK = attachment.networkParameters.getPackageOwnerOf(attachment.contractAttachment.allContracts)
|
||||
if (packageOwnerPK == null) {
|
||||
log.warn("Missing registered java package owner for ${attachment.contractAttachment.contract} in network parameters: ${attachment.networkParameters} (input constraint = $input, output constraint = $output)")
|
||||
return false
|
||||
}
|
||||
else if (!packageOwnerPK.isFulfilledBy(output.key) ) {
|
||||
log.warn("Java package owner keys do not match signature constrained output state keys")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
@ -108,9 +131,8 @@ data class HashAttachmentConstraint(val attachmentId: SecureHash) : AttachmentCo
|
||||
object WhitelistedByZoneAttachmentConstraint : AttachmentConstraint {
|
||||
override fun isSatisfiedBy(attachment: Attachment): Boolean {
|
||||
return if (attachment is AttachmentWithContext) {
|
||||
val whitelist = attachment.whitelistedContractImplementations
|
||||
?: throw IllegalStateException("Unable to verify WhitelistedByZoneAttachmentConstraint - whitelist not specified")
|
||||
attachment.id in (whitelist[attachment.stateContract] ?: emptyList())
|
||||
val whitelist = attachment.networkParameters.whitelistedContractImplementations
|
||||
attachment.id in (whitelist[attachment.contract] ?: emptyList())
|
||||
} else false
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.internal.UNKNOWN_VERSION
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.security.PublicKey
|
||||
|
||||
@ -18,11 +19,14 @@ class ContractAttachment @JvmOverloads constructor(
|
||||
val contract: ContractClassName,
|
||||
val additionalContracts: Set<ContractClassName> = emptySet(),
|
||||
val uploader: String? = null,
|
||||
override val signerKeys: List<PublicKey> = emptyList()) : Attachment by attachment {
|
||||
override val signerKeys: List<PublicKey> = emptyList(),
|
||||
val version: String = UNKNOWN_VERSION) : Attachment by attachment {
|
||||
|
||||
val allContracts: Set<ContractClassName> get() = additionalContracts + contract
|
||||
|
||||
val isSigned: Boolean get() = signerKeys.isNotEmpty()
|
||||
|
||||
override fun toString(): String {
|
||||
return "ContractAttachment(attachment=${attachment.id}, contracts='$allContracts', uploader='$uploader')"
|
||||
return "ContractAttachment(attachment=${attachment.id}, contracts='$allContracts', uploader='$uploader', signed='$isSigned', version='$version')"
|
||||
}
|
||||
}
|
@ -20,6 +20,7 @@ const val DEPLOYED_CORDAPP_UPLOADER = "app"
|
||||
const val RPC_UPLOADER = "rpc"
|
||||
const val P2P_UPLOADER = "p2p"
|
||||
const val UNKNOWN_UPLOADER = "unknown"
|
||||
const val UNKNOWN_VERSION = "unknown"
|
||||
|
||||
private val TRUSTED_UPLOADERS = listOf(DEPLOYED_CORDAPP_UPLOADER, RPC_UPLOADER)
|
||||
|
||||
|
@ -1,21 +1,19 @@
|
||||
package net.corda.core.internal
|
||||
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.contracts.ContractAttachment
|
||||
import net.corda.core.contracts.ContractClassName
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.node.NetworkParameters
|
||||
|
||||
/**
|
||||
* Used only for passing to the Attachment constraint verification.
|
||||
*/
|
||||
class AttachmentWithContext(
|
||||
val contractAttachment: ContractAttachment,
|
||||
val stateContract: ContractClassName,
|
||||
/** Required for verifying [WhitelistedByZoneAttachmentConstraint] */
|
||||
val whitelistedContractImplementations: Map<String, List<AttachmentId>>?
|
||||
val contract: ContractClassName,
|
||||
/** Required for verifying [WhitelistedByZoneAttachmentConstraint] and [HashAttachmentConstraint] migration to [SignatureAttachmentConstraint] */
|
||||
val networkParameters: NetworkParameters
|
||||
) : Attachment by contractAttachment {
|
||||
init {
|
||||
require(stateContract in contractAttachment.allContracts) {
|
||||
require(contract in contractAttachment.allContracts) {
|
||||
"This AttachmentWithContext was not initialised properly"
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,10 @@ package net.corda.core.node
|
||||
|
||||
import net.corda.core.CordaRuntimeException
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.contracts.ContractClassName
|
||||
import net.corda.core.crypto.toStringShort
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.VisibleForTesting
|
||||
import net.corda.core.internal.requirePackageValid
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
@ -185,7 +187,17 @@ data class NetworkParameters(
|
||||
/**
|
||||
* Returns the public key of the package owner of the [contractClassName], or null if not owned.
|
||||
*/
|
||||
fun getOwnerOf(contractClassName: String): PublicKey? = this.packageOwnership.filterKeys { packageName -> owns(packageName, contractClassName) }.values.singleOrNull()
|
||||
@VisibleForTesting
|
||||
internal fun getPackageOwnerOf(contractClassName: ContractClassName): PublicKey? = this.packageOwnership.filterKeys { packageName -> owns(packageName, contractClassName) }.values.singleOrNull()
|
||||
|
||||
/**
|
||||
* Returns the public key of the package owner if any of [contractClassName] match, or null if not owned.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
internal fun getPackageOwnerOf(contractClassNames: Set<ContractClassName>): PublicKey? {
|
||||
val ownerKeys = contractClassNames.map { getPackageOwnerOf(it) }
|
||||
return ownerKeys.find { it != null }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the only properties changed in [newNetworkParameters] are [AutoAcceptable] and not
|
||||
|
@ -3,8 +3,7 @@ package net.corda.core.node.services
|
||||
import net.corda.core.DoNotImplement
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.node.services.vault.AttachmentQueryCriteria
|
||||
import net.corda.core.node.services.vault.AttachmentSort
|
||||
import net.corda.core.node.services.vault.*
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.nio.file.FileAlreadyExistsException
|
||||
@ -69,5 +68,11 @@ interface AttachmentStorage {
|
||||
* @return true if it's in there
|
||||
*/
|
||||
fun hasAttachment(attachmentId: AttachmentId): Boolean
|
||||
|
||||
// Note: cannot apply @JvmOverloads to interfaces nor interface implementations.
|
||||
// Java Helpers.
|
||||
fun queryAttachments(criteria: AttachmentQueryCriteria): List<AttachmentId> {
|
||||
return queryAttachments(criteria, null)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
package net.corda.core.node.services.vault
|
||||
|
||||
import net.corda.core.DoNotImplement
|
||||
import net.corda.core.contracts.ContractClassName
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.UniqueIdentifier
|
||||
@ -11,6 +12,7 @@ import net.corda.core.node.services.Vault
|
||||
import net.corda.core.schemas.StatePersistable
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import javax.persistence.criteria.Predicate
|
||||
@ -275,12 +277,32 @@ sealed class AttachmentQueryCriteria : GenericQueryCriteria<AttachmentQueryCrite
|
||||
/**
|
||||
* AttachmentsQueryCriteria:
|
||||
*/
|
||||
data class AttachmentsQueryCriteria @JvmOverloads constructor (val uploaderCondition: ColumnPredicate<String>? = null,
|
||||
val filenameCondition: ColumnPredicate<String>? = null,
|
||||
val uploadDateCondition: ColumnPredicate<Instant>? = null) : AttachmentQueryCriteria() {
|
||||
data class AttachmentsQueryCriteria @JvmOverloads constructor(val uploaderCondition: ColumnPredicate<String>? = null,
|
||||
val filenameCondition: ColumnPredicate<String>? = null,
|
||||
val uploadDateCondition: ColumnPredicate<Instant>? = null,
|
||||
val contractClassNamesCondition: ColumnPredicate<List<ContractClassName>>? = null,
|
||||
val signersCondition: ColumnPredicate<List<PublicKey>>? = null,
|
||||
val isSignedCondition: ColumnPredicate<Boolean>? = null,
|
||||
val versionCondition: ColumnPredicate<List<String>>? = null) : AttachmentQueryCriteria() {
|
||||
override fun visit(parser: AttachmentsQueryCriteriaParser): Collection<Predicate> {
|
||||
return parser.parseCriteria(this)
|
||||
}
|
||||
|
||||
fun copy(
|
||||
uploaderCondition: ColumnPredicate<String>? = this.uploaderCondition,
|
||||
filenameCondition: ColumnPredicate<String>? = this.filenameCondition,
|
||||
uploadDateCondition: ColumnPredicate<Instant>? = this.uploadDateCondition
|
||||
): AttachmentsQueryCriteria {
|
||||
return AttachmentsQueryCriteria(uploaderCondition, filenameCondition, uploadDateCondition)
|
||||
}
|
||||
|
||||
fun withUploader(uploaderPredicate: ColumnPredicate<String>) = copy(uploaderCondition = uploaderPredicate)
|
||||
fun withFilename(filenamePredicate: ColumnPredicate<String>) = copy(filenameCondition = filenamePredicate)
|
||||
fun withUploadDate(uploadDatePredicate: ColumnPredicate<Instant>) = copy(uploadDateCondition = uploadDatePredicate)
|
||||
fun withContractClassNames(contractClassNamesPredicate: ColumnPredicate<List<ContractClassName>>) = copy(contractClassNamesCondition = contractClassNamesPredicate)
|
||||
fun withSigners(signersPredicate: ColumnPredicate<List<PublicKey>>) = copy(signersCondition = signersPredicate)
|
||||
fun isSigned(isSignedPredicate: ColumnPredicate<Boolean>) = copy(isSignedCondition = isSignedPredicate)
|
||||
fun withVersions(versionsPredicate: ColumnPredicate<List<String>>) = copy(versionCondition = versionsPredicate)
|
||||
}
|
||||
|
||||
class AndComposition(override val a: AttachmentQueryCriteria, override val b: AttachmentQueryCriteria): AttachmentQueryCriteria(), GenericQueryCriteria.ChainableQueryCriteria.AndVisitor<AttachmentQueryCriteria, AttachmentsQueryCriteriaParser, AttachmentSort>
|
||||
|
@ -2,14 +2,18 @@ package net.corda.core.serialization.internal
|
||||
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.contracts.ContractAttachment
|
||||
import net.corda.core.contracts.TransactionVerificationException
|
||||
import net.corda.core.contracts.TransactionVerificationException.OverlappingAttachmentsException
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.internal.VisibleForTesting
|
||||
import net.corda.core.internal.createSimpleCache
|
||||
import net.corda.core.internal.isUploaderTrusted
|
||||
import net.corda.core.internal.toSynchronised
|
||||
import net.corda.core.serialization.SerializationFactory
|
||||
import net.corda.core.serialization.internal.AttachmentURLStreamHandlerFactory.toUrl
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.net.*
|
||||
@ -24,7 +28,15 @@ import java.util.jar.Manifest
|
||||
class AttachmentsClassLoader(attachments: List<Attachment>, parent: ClassLoader = ClassLoader.getSystemClassLoader()) :
|
||||
URLClassLoader(attachments.map(::toUrl).toTypedArray(), parent) {
|
||||
|
||||
init {
|
||||
require(attachments.mapNotNull { it as? ContractAttachment }.all { isUploaderTrusted(it.uploader) }) {
|
||||
"Attempting to load Contract Attachments downloaded from the network"
|
||||
}
|
||||
requireNoDuplicates(attachments)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = contextLogger()
|
||||
|
||||
init {
|
||||
// This is required to register the AttachmentURLStreamHandlerFactory.
|
||||
@ -45,13 +57,22 @@ class AttachmentsClassLoader(attachments: List<Attachment>, parent: ClassLoader
|
||||
}
|
||||
|
||||
private fun requireNoDuplicates(attachments: List<Attachment>) {
|
||||
val classLoaderEntries = mutableSetOf<String>()
|
||||
// Avoid unnecessary duplicate checking if possible:
|
||||
// 1. single attachment.
|
||||
// 2. multiple attachments with non-overlapping contract classes.
|
||||
if (attachments.size <= 1) return
|
||||
val overlappingContractClasses = attachments.mapNotNull { it as? ContractAttachment }.flatMap { it.allContracts }.groupingBy { it }.eachCount().filter { it.value > 1 }
|
||||
if (overlappingContractClasses.isEmpty()) return
|
||||
|
||||
// this logic executes only if there are overlapping contract classes
|
||||
log.debug("Duplicate contract class checking for $overlappingContractClasses")
|
||||
val classLoaderEntries = mutableMapOf<String, Attachment>()
|
||||
for (attachment in attachments) {
|
||||
attachment.openAsJAR().use { jar ->
|
||||
val targetPlatformVersion = jar.manifest?.targetPlatformVersion ?: 1
|
||||
while (true) {
|
||||
val entry = jar.nextJarEntry ?: break
|
||||
|
||||
if (entry.isDirectory) continue
|
||||
// We already verified that paths are not strange/game playing when we inserted the attachment
|
||||
// into the storage service. So we don't need to repeat it here.
|
||||
//
|
||||
@ -59,14 +80,29 @@ class AttachmentsClassLoader(attachments: List<Attachment>, parent: ClassLoader
|
||||
// filesystem tries to be case insensitive. This may break developers who attempt to use ProGuard.
|
||||
//
|
||||
// Also convert to Unix path separators as all resource/class lookups will expect this.
|
||||
//
|
||||
val path = entry.name.toLowerCase().replace('\\', '/')
|
||||
// TODO - If 2 entries are identical, it means the same file is present in both attachments, so that should be ok.
|
||||
if (shouldCheckForNoOverlap(path, targetPlatformVersion)) {
|
||||
if (path in classLoaderEntries) throw TransactionVerificationException.OverlappingAttachmentsException(path)
|
||||
classLoaderEntries.add(path)
|
||||
if (path in classLoaderEntries.keys) {
|
||||
// If 2 entries have the same content hash, it means the same file is present in both attachments, so that is ok.
|
||||
val contentHash = readAttachment(attachment, path).sha256()
|
||||
val originalAttachment = classLoaderEntries[path]!!
|
||||
val originalContentHash = readAttachment(originalAttachment, path).sha256()
|
||||
if (contentHash == originalContentHash) {
|
||||
log.debug { "Duplicate entry $path has same content hash $contentHash" }
|
||||
continue
|
||||
} else {
|
||||
log.debug { "Content hash differs for $path" }
|
||||
throw OverlappingAttachmentsException(path)
|
||||
}
|
||||
}
|
||||
log.debug { "Adding new entry for $path" }
|
||||
classLoaderEntries[path] = attachment
|
||||
}
|
||||
}
|
||||
}
|
||||
log.debug { "${classLoaderEntries.size} classloaded entries for $attachment" }
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,14 +113,14 @@ class AttachmentsClassLoader(attachments: List<Attachment>, parent: ClassLoader
|
||||
val minPlatformVersion = mainAttributes.getValue("Min-Platform-Version")?.toInt() ?: 1
|
||||
return mainAttributes.getValue("Target-Platform-Version")?.toInt() ?: minPlatformVersion
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
require(attachments.mapNotNull { it as? ContractAttachment }.all { isUploaderTrusted(it.uploader) }) {
|
||||
"Attempting to load Contract Attachments downloaded from the network"
|
||||
@VisibleForTesting
|
||||
private fun readAttachment(attachment: Attachment, filepath: String): ByteArray {
|
||||
ByteArrayOutputStream().use {
|
||||
attachment.extractFile(filepath, it)
|
||||
return it.toByteArray()
|
||||
}
|
||||
}
|
||||
|
||||
requireNoDuplicates(attachments)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -266,8 +266,7 @@ data class ContractUpgradeLedgerTransaction(
|
||||
legacyContractAttachment as? ContractAttachment
|
||||
?: ContractAttachment(legacyContractAttachment, legacyContractClassName, signerKeys = legacyContractAttachment.signerKeys),
|
||||
upgradedContract.legacyContract,
|
||||
networkParameters.whitelistedContractImplementations
|
||||
)
|
||||
networkParameters)
|
||||
|
||||
// TODO: exclude encumbrance states from this check
|
||||
check(inputs.all { it.state.constraint.isSatisfiedBy(attachmentForConstraintVerification) }) {
|
||||
|
@ -128,7 +128,7 @@ private constructor(
|
||||
logger.warn("Network parameters on the LedgerTransaction with id: $id are null. Please don't use deprecated constructors of the LedgerTransaction. " +
|
||||
"Use WireTransaction.toLedgerTransaction instead. The result of the verify method might not be accurate.")
|
||||
}
|
||||
val contractAttachmentsByContract: Map<ContractClassName, ContractAttachment> = getUniqueContractAttachmentsByContract()
|
||||
val contractAttachmentsByContract: Map<ContractClassName, Set<ContractAttachment>> = getContractAttachmentsByContract(allStates.map { it.contract }.toSet())
|
||||
|
||||
AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(this.attachments) { transactionClassLoader ->
|
||||
|
||||
@ -137,8 +137,8 @@ private constructor(
|
||||
// TODO - verify for version downgrade
|
||||
validatePackageOwnership(contractAttachmentsByContract)
|
||||
validateStatesAgainstContract(internalTx)
|
||||
verifyConstraintsValidity(internalTx, contractAttachmentsByContract, transactionClassLoader)
|
||||
verifyConstraints(internalTx, contractAttachmentsByContract)
|
||||
val hashToSignatureConstrainedContracts = verifyConstraintsValidity(internalTx, contractAttachmentsByContract, transactionClassLoader)
|
||||
verifyConstraints(internalTx, contractAttachmentsByContract, hashToSignatureConstrainedContracts)
|
||||
verifyContracts(internalTx)
|
||||
}
|
||||
}
|
||||
@ -179,21 +179,17 @@ private constructor(
|
||||
* TODO - revisit once transaction contains network parameters. - UPDATE: It contains them, but because of the API stability and the fact that
|
||||
* LedgerTransaction was data class i.e. exposed constructors that shouldn't had been exposed, we still need to keep them nullable :/
|
||||
*/
|
||||
private fun validatePackageOwnership(contractAttachmentsByContract: Map<ContractClassName, ContractAttachment>) {
|
||||
// This should never happen once we have network parameters in the transaction.
|
||||
if (networkParameters == null) {
|
||||
return
|
||||
}
|
||||
private fun validatePackageOwnership(contractAttachmentsByContract: Map<ContractClassName, Set<ContractAttachment>>) {
|
||||
val contractsAndOwners = allStates.mapNotNull { transactionState ->
|
||||
val contractClassName = transactionState.contract
|
||||
networkParameters.getOwnerOf(contractClassName)?.let { contractClassName to it }
|
||||
networkParameters!!.getPackageOwnerOf(contractClassName)?.let { contractClassName to it }
|
||||
}.toMap()
|
||||
|
||||
contractsAndOwners.forEach { contract, owner ->
|
||||
val attachment = contractAttachmentsByContract[contract]!!
|
||||
if (!owner.isFulfilledBy(attachment.signerKeys)) {
|
||||
throw TransactionVerificationException.ContractAttachmentNotSignedByPackageOwnerException(this.id, id, contract)
|
||||
}
|
||||
contractAttachmentsByContract[contract]?.filter { it.isSigned }?.forEach { attachment ->
|
||||
if (!owner.isFulfilledBy(attachment.signerKeys))
|
||||
throw TransactionVerificationException.ContractAttachmentNotSignedByPackageOwnerException(this.id, id, contract)
|
||||
} ?: throw TransactionVerificationException.ContractAttachmentNotSignedByPackageOwnerException(this.id, id, contract)
|
||||
}
|
||||
}
|
||||
|
||||
@ -201,10 +197,10 @@ private constructor(
|
||||
* Enforces the validity of the actual constraints.
|
||||
* * Constraints should be one of the valid supported ones.
|
||||
* * Constraints should propagate correctly if not marked otherwise.
|
||||
*
|
||||
* Returns set of contract classes that identify hash -> signature constraint switchover
|
||||
*/
|
||||
private fun verifyConstraintsValidity(internalTx: LedgerTransaction,
|
||||
contractAttachmentsByContract: Map<ContractClassName, ContractAttachment>,
|
||||
transactionClassLoader: ClassLoader) {
|
||||
private fun verifyConstraintsValidity(internalTx: LedgerTransaction, contractAttachmentsByContract: Map<ContractClassName, Set<ContractAttachment>>, transactionClassLoader: ClassLoader): MutableSet<ContractClassName> {
|
||||
// First check that the constraints are valid.
|
||||
for (state in internalTx.allStates) {
|
||||
checkConstraintValidity(state)
|
||||
@ -215,6 +211,9 @@ private constructor(
|
||||
val inputContractGroups = internalTx.inputs.groupBy { it.state.contract }
|
||||
val outputContractGroups = internalTx.outputs.groupBy { it.contract }
|
||||
|
||||
// identify any contract classes where input-output pair are transitioning from hash to signature constraints.
|
||||
val hashToSignatureConstrainedContracts = mutableSetOf<ContractClassName>()
|
||||
|
||||
for (contractClassName in (inputContractGroups.keys + outputContractGroups.keys)) {
|
||||
if (contractClassName.contractHasAutomaticConstraintPropagation(transactionClassLoader)) {
|
||||
// Verify that the constraints of output states have at least the same level of restriction as the constraints of the corresponding input states.
|
||||
@ -222,15 +221,31 @@ private constructor(
|
||||
val outputConstraints = outputContractGroups[contractClassName]?.map { it.constraint }?.toSet()
|
||||
outputConstraints?.forEach { outputConstraint ->
|
||||
inputConstraints?.forEach { inputConstraint ->
|
||||
if (!(outputConstraint.canBeTransitionedFrom(inputConstraint, contractAttachmentsByContract[contractClassName]!!))) {
|
||||
val constraintAttachment = resolveAttachment(contractClassName, contractAttachmentsByContract)
|
||||
if (!(outputConstraint.canBeTransitionedFrom(inputConstraint, constraintAttachment))) {
|
||||
throw TransactionVerificationException.ConstraintPropagationRejection(id, contractClassName, inputConstraint, outputConstraint)
|
||||
}
|
||||
// Hash to signature constraints auto-migration
|
||||
if (outputConstraint is SignatureAttachmentConstraint && inputConstraint is HashAttachmentConstraint)
|
||||
hashToSignatureConstrainedContracts.add(contractClassName)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
contractClassName.warnContractWithoutConstraintPropagation()
|
||||
}
|
||||
}
|
||||
return hashToSignatureConstrainedContracts
|
||||
}
|
||||
|
||||
private fun resolveAttachment(contractClassName: ContractClassName, contractAttachmentsByContract: Map<ContractClassName, Set<ContractAttachment>>): AttachmentWithContext {
|
||||
val unsignedAttachment = contractAttachmentsByContract[contractClassName]!!.filter { !it.isSigned }.firstOrNull()
|
||||
val signedAttachment = contractAttachmentsByContract[contractClassName]!!.filter { it.isSigned }.firstOrNull()
|
||||
return when {
|
||||
(unsignedAttachment != null && signedAttachment != null) -> AttachmentWithContext(signedAttachment, contractClassName, networkParameters!!)
|
||||
(unsignedAttachment != null) -> AttachmentWithContext(unsignedAttachment, contractClassName, networkParameters!!)
|
||||
(signedAttachment != null) -> AttachmentWithContext(signedAttachment, contractClassName, networkParameters!!)
|
||||
else -> throw TransactionVerificationException.ContractConstraintRejection(id, contractClassName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -240,39 +255,64 @@ private constructor(
|
||||
*
|
||||
* @throws TransactionVerificationException if the constraints fail to verify
|
||||
*/
|
||||
private fun verifyConstraints(internalTx: LedgerTransaction, contractAttachmentsByContract: Map<ContractClassName, ContractAttachment>) {
|
||||
private fun verifyConstraints(internalTx: LedgerTransaction, contractAttachmentsByContract: Map<ContractClassName, Set<ContractAttachment>>, hashToSignatureConstrainedContracts: MutableSet<ContractClassName>) {
|
||||
for (state in internalTx.allStates) {
|
||||
val contractAttachment = contractAttachmentsByContract[state.contract]
|
||||
?: throw TransactionVerificationException.MissingAttachmentRejection(id, state.contract)
|
||||
|
||||
val constraintAttachment = AttachmentWithContext(contractAttachment, state.contract,
|
||||
networkParameters?.whitelistedContractImplementations)
|
||||
|
||||
if (state.constraint is SignatureAttachmentConstraint)
|
||||
checkMinimumPlatformVersion(networkParameters?.minimumPlatformVersion ?: 1, 4, "Signature constraints")
|
||||
checkMinimumPlatformVersion(networkParameters!!.minimumPlatformVersion, 4, "Signature constraints")
|
||||
|
||||
val constraintAttachment =
|
||||
// hash to to signature constraint migration logic:
|
||||
// pass the unsigned attachment when verifying the constraint of the input state, and the signed attachment when verifying the constraint of the output state.
|
||||
if (state.contract in hashToSignatureConstrainedContracts) {
|
||||
val unsignedAttachment = contractAttachmentsByContract[state.contract].unsigned
|
||||
?: throw TransactionVerificationException.MissingAttachmentRejection(id, state.contract)
|
||||
val signedAttachment = contractAttachmentsByContract[state.contract].signed
|
||||
?: throw TransactionVerificationException.MissingAttachmentRejection(id, state.contract)
|
||||
when {
|
||||
// use unsigned attachment if hash-constrained input state
|
||||
state.data in inputStates -> AttachmentWithContext(unsignedAttachment, state.contract, networkParameters!!)
|
||||
// use signed attachment if signature-constrained output state
|
||||
state.data in outputStates -> AttachmentWithContext(signedAttachment, state.contract, networkParameters!!)
|
||||
else -> throw IllegalStateException("${state.contract} must use either signed or unsigned attachment in hash to signature constraints migration")
|
||||
}
|
||||
}
|
||||
// standard processing logic
|
||||
else {
|
||||
val contractAttachment = contractAttachmentsByContract[state.contract]?.firstOrNull()
|
||||
?: throw TransactionVerificationException.MissingAttachmentRejection(id, state.contract)
|
||||
AttachmentWithContext(contractAttachment, state.contract, networkParameters!!)
|
||||
}
|
||||
|
||||
if (!state.constraint.isSatisfiedBy(constraintAttachment)) {
|
||||
throw TransactionVerificationException.ContractConstraintRejection(id, state.contract)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getUniqueContractAttachmentsByContract(): Map<ContractClassName, ContractAttachment> {
|
||||
val result = mutableMapOf<ContractClassName, ContractAttachment>()
|
||||
private val Set<ContractAttachment>?.unsigned: ContractAttachment?
|
||||
get() {
|
||||
return this?.filter { !it.isSigned }?.firstOrNull()
|
||||
}
|
||||
|
||||
private val Set<ContractAttachment>?.signed: ContractAttachment?
|
||||
get() {
|
||||
return this?.filter { it.isSigned }?.firstOrNull()
|
||||
}
|
||||
|
||||
// TODO: revisit to include contract version information
|
||||
/**
|
||||
* This method may return more than one attachment for a given contract class.
|
||||
* Specifically, this is the case for transactions combining hash and signature constraints where the hash constrained contract jar
|
||||
* will be unsigned, and the signature constrained counterpart will be signed.
|
||||
*/
|
||||
private fun getContractAttachmentsByContract(contractClasses: Set<ContractClassName>): Map<ContractClassName, Set<ContractAttachment>> {
|
||||
val result = mutableMapOf<ContractClassName, Set<ContractAttachment>>()
|
||||
|
||||
for (attachment in attachments) {
|
||||
if (attachment !is ContractAttachment) continue
|
||||
|
||||
for (contract in attachment.allContracts) {
|
||||
result.compute(contract) { _, previousAttachment ->
|
||||
when {
|
||||
previousAttachment == null -> attachment
|
||||
attachment.id == previousAttachment.id -> previousAttachment
|
||||
// In case multiple attachments have been added for the same contract, fail because this
|
||||
// transaction will not be able to be verified because it will break the no-overlap rule
|
||||
// that we have implemented in our Classloaders
|
||||
else -> throw TransactionVerificationException.ConflictingAttachmentsRejection(id, contract)
|
||||
}
|
||||
}
|
||||
for (contract in contractClasses) {
|
||||
if (!attachment.allContracts.contains(contract)) continue
|
||||
result[contract] = result.getOrDefault(contract, setOf(attachment)).plus(attachment)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,8 @@ import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.node.ZoneVersionTooLowException
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
import net.corda.core.node.services.KeyManagementService
|
||||
import net.corda.core.node.services.vault.AttachmentQueryCriteria
|
||||
import net.corda.core.node.services.vault.Builder
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializationFactory
|
||||
import net.corda.core.utilities.contextLogger
|
||||
@ -175,9 +177,9 @@ open class TransactionBuilder @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
// For each contract, resolve the AutomaticPlaceholderConstraint, and select the attachment.
|
||||
val contractAttachmentsAndResolvedOutputStates: List<Pair<AttachmentId, List<TransactionState<ContractState>>?>> = allContracts.toSet()
|
||||
val contractAttachmentsAndResolvedOutputStates: List<Pair<Set<AttachmentId>, List<TransactionState<ContractState>>?>> = allContracts.toSet()
|
||||
.map { ctr ->
|
||||
handleContract(ctr, inputContractGroups[ctr], outputContractGroups[ctr], explicitAttachmentContractsMap[ctr], serializationContext, services)
|
||||
handleContract(ctr, inputContractGroups[ctr], outputContractGroups[ctr], explicitAttachmentContractsMap[ctr], services)
|
||||
}
|
||||
|
||||
val resolvedStates: List<TransactionState<ContractState>> = contractAttachmentsAndResolvedOutputStates.mapNotNull { it.second }
|
||||
@ -186,7 +188,7 @@ open class TransactionBuilder @JvmOverloads constructor(
|
||||
// The output states need to preserve the order in which they were added.
|
||||
val resolvedOutputStatesInTheOriginalOrder: List<TransactionState<ContractState>> = outputStates().map { os -> resolvedStates.find { rs -> rs.data == os.data && rs.encumbrance == os.encumbrance }!! }
|
||||
|
||||
val attachments: Collection<AttachmentId> = contractAttachmentsAndResolvedOutputStates.map { it.first } + refStateContractAttachments
|
||||
val attachments: Collection<AttachmentId> = contractAttachmentsAndResolvedOutputStates.flatMap { it.first } + refStateContractAttachments
|
||||
|
||||
return Pair(attachments, resolvedOutputStatesInTheOriginalOrder)
|
||||
}
|
||||
@ -213,11 +215,39 @@ open class TransactionBuilder @JvmOverloads constructor(
|
||||
inputStates: List<TransactionState<ContractState>>?,
|
||||
outputStates: List<TransactionState<ContractState>>?,
|
||||
explicitContractAttachment: AttachmentId?,
|
||||
serializationContext: SerializationContext?,
|
||||
services: ServicesForResolution
|
||||
): Pair<AttachmentId, List<TransactionState<ContractState>>?> {
|
||||
): Pair<Set<AttachmentId>, List<TransactionState<ContractState>>?> {
|
||||
val inputsAndOutputs = (inputStates ?: emptyList()) + (outputStates ?: emptyList())
|
||||
|
||||
// Hash to Signature constraints migration switchover
|
||||
// identify if any input-output pairs are transitioning from hash to signature constraints:
|
||||
// 1. output states contain implicitly selected hash constraint (pre-existing from set of unconsumed states in a nodes vault) or explicitly set SignatureConstraint
|
||||
// 2. node has signed jar for associated contract class and version
|
||||
val inputsHashConstraints = inputStates?.filter { it.constraint is HashAttachmentConstraint } ?: emptyList()
|
||||
val outputHashConstraints = outputStates?.filter { it.constraint is HashAttachmentConstraint } ?: emptyList()
|
||||
val outputSignatureConstraints = outputStates?.filter { it.constraint is SignatureAttachmentConstraint } ?: emptyList()
|
||||
if (inputsHashConstraints.isNotEmpty() && (outputHashConstraints.isNotEmpty() || outputSignatureConstraints.isNotEmpty())) {
|
||||
val attachmentQueryCriteria = AttachmentQueryCriteria.AttachmentsQueryCriteria(contractClassNamesCondition = Builder.equal(listOf(contractClassName)))
|
||||
val attachmentIds = services.attachments.queryAttachments(attachmentQueryCriteria)
|
||||
// only switchover if we have both signed and unsigned attachments for the given contract class name
|
||||
if (attachmentIds.isNotEmpty() && attachmentIds.size == 2) {
|
||||
val attachmentsToUse = attachmentIds.map {
|
||||
services.attachments.openAttachment(it)?.let { it as ContractAttachment }
|
||||
?: throw IllegalArgumentException("Contract attachment $it for $contractClassName is missing.")
|
||||
}
|
||||
val signedAttachment = attachmentsToUse.filter { it.isSigned }.firstOrNull() ?: throw IllegalArgumentException("Signed contract attachment for $contractClassName is missing.")
|
||||
val outputConstraints =
|
||||
if (outputHashConstraints.isNotEmpty()) {
|
||||
log.warn("Switching output states from hash to signed constraints using signers in signed contract attachment given by ${signedAttachment.id}")
|
||||
val outputsSignatureConstraints = outputHashConstraints.map { it.copy(constraint = SignatureAttachmentConstraint(signedAttachment.signerKeys.first())) }
|
||||
outputs.addAll(outputsSignatureConstraints)
|
||||
outputs.removeAll(outputHashConstraints)
|
||||
outputsSignatureConstraints
|
||||
} else outputSignatureConstraints
|
||||
return Pair(attachmentIds.toSet(), outputConstraints)
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if there are any HashConstraints that pin the version of a contract. If there are, check if we trust them.
|
||||
val hashAttachments = inputsAndOutputs
|
||||
.filter { it.constraint is HashAttachmentConstraint }
|
||||
@ -234,11 +264,6 @@ open class TransactionBuilder @JvmOverloads constructor(
|
||||
require(hashAttachments.size <= 1) {
|
||||
"Transaction was built with $contractClassName states with multiple HashConstraints. This is illegal, because it makes it impossible to validate with a single version of the contract code."
|
||||
}
|
||||
if (explicitContractAttachment != null && hashAttachments.singleOrNull() != null) {
|
||||
require(explicitContractAttachment == (hashAttachments.single() as ContractAttachment).attachment.id) {
|
||||
"An attachment has been explicitly set for contract $contractClassName in the transaction builder which conflicts with the HashConstraint of a state."
|
||||
}
|
||||
}
|
||||
|
||||
// This will contain the hash of the JAR that *has* to be used by this Transaction, because it is explicit. Or null if none.
|
||||
val forcedAttachmentId = explicitContractAttachment ?: hashAttachments.singleOrNull()?.id
|
||||
@ -257,12 +282,12 @@ open class TransactionBuilder @JvmOverloads constructor(
|
||||
|
||||
// For Exit transactions (no output states) there is no need to resolve the output constraints.
|
||||
if (outputStates == null) {
|
||||
return Pair(selectedAttachmentId, null)
|
||||
return Pair(setOf(selectedAttachmentId), null)
|
||||
}
|
||||
|
||||
// If there are no automatic constraints, there is nothing to resolve.
|
||||
if (outputStates.none { it.constraint in automaticConstraints }) {
|
||||
return Pair(selectedAttachmentId, outputStates)
|
||||
return Pair(setOf(selectedAttachmentId), outputStates)
|
||||
}
|
||||
|
||||
// The final step is to resolve AutomaticPlaceholderConstraint.
|
||||
@ -275,7 +300,7 @@ open class TransactionBuilder @JvmOverloads constructor(
|
||||
val defaultOutputConstraint = selectAttachmentConstraint(contractClassName, inputStates, attachmentToUse, services)
|
||||
|
||||
// Sanity check that the selected attachment actually passes.
|
||||
val constraintAttachment = AttachmentWithContext(attachmentToUse, contractClassName, services.networkParameters.whitelistedContractImplementations)
|
||||
val constraintAttachment = AttachmentWithContext(attachmentToUse, contractClassName, services.networkParameters)
|
||||
require(defaultOutputConstraint.isSatisfiedBy(constraintAttachment)) { "Selected output constraint: $defaultOutputConstraint not satisfying $selectedAttachmentId" }
|
||||
|
||||
val resolvedOutputStates = outputStates.map {
|
||||
@ -285,14 +310,14 @@ open class TransactionBuilder @JvmOverloads constructor(
|
||||
} else {
|
||||
// If the constraint on the output state is already set, and is not a valid transition or can't be transitioned, then fail early.
|
||||
inputStates?.forEach { input ->
|
||||
require(outputConstraint.canBeTransitionedFrom(input.constraint, attachmentToUse)) { "Output state constraint $outputConstraint cannot be transitions from ${input.constraint}" }
|
||||
require(outputConstraint.canBeTransitionedFrom(input.constraint, constraintAttachment)) { "Output state constraint $outputConstraint cannot be transitioned from ${input.constraint}" }
|
||||
}
|
||||
require(outputConstraint.isSatisfiedBy(constraintAttachment)) { "Output state constraint check fails. $outputConstraint" }
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
return Pair(selectedAttachmentId, resolvedOutputStates)
|
||||
return Pair(setOf(selectedAttachmentId), resolvedOutputStates)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -134,7 +134,6 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
||||
return toLedgerTransactionInternal(resolveIdentity, resolveAttachment, { stateRef -> resolveStateRef(stateRef)?.serialize() }, resolveParameters)
|
||||
}
|
||||
|
||||
|
||||
private fun toLedgerTransactionInternal(
|
||||
resolveIdentity: (PublicKey) -> Party?,
|
||||
resolveAttachment: (SecureHash) -> Attachment?,
|
||||
|
@ -4,8 +4,13 @@ import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.mock
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.SecureHash.Companion.allOnesHash
|
||||
import net.corda.core.crypto.SecureHash.Companion.zeroHash
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.AttachmentWithContext
|
||||
import net.corda.core.internal.inputStream
|
||||
import net.corda.core.internal.toPath
|
||||
import net.corda.core.node.NotaryInfo
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.finance.POUNDS
|
||||
@ -13,20 +18,26 @@ import net.corda.finance.`issued by`
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.node.services.api.IdentityServiceInternal
|
||||
import net.corda.testing.common.internal.testNetworkParameters
|
||||
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
import net.corda.testing.core.TestIdentity
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.testing.core.internal.JarSignatureTestUtils.generateKey
|
||||
import net.corda.testing.core.internal.ContractJarTestUtils
|
||||
import net.corda.testing.core.internal.SelfCleaningDir
|
||||
import net.corda.testing.internal.MockCordappProvider
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.ledger
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.*
|
||||
import java.security.PublicKey
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class ConstraintsPropagationTests {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule()
|
||||
|
||||
private companion object {
|
||||
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
|
||||
val ALICE = TestIdentity(CordaX500Name("ALICE", "London", "GB"))
|
||||
@ -36,28 +47,44 @@ class ConstraintsPropagationTests {
|
||||
val BOB_PARTY get() = BOB.party
|
||||
val BOB_PUBKEY get() = BOB.publicKey
|
||||
val noPropagationContractClassName = "net.corda.core.contracts.NoPropagationContract"
|
||||
val propagatingContractClassName = "net.corda.core.contracts.PropagationContract"
|
||||
|
||||
private lateinit var keyStoreDir: SelfCleaningDir
|
||||
private lateinit var hashToSignatureConstraintsKey: PublicKey
|
||||
|
||||
@BeforeClass
|
||||
@JvmStatic
|
||||
fun setUpBeforeClass() {
|
||||
keyStoreDir = SelfCleaningDir()
|
||||
hashToSignatureConstraintsKey = keyStoreDir.path.generateKey("testAlias", "testPassword", ALICE_NAME.toString())
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
@JvmStatic
|
||||
fun cleanUpAfterClass() {
|
||||
keyStoreDir.close()
|
||||
}
|
||||
}
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule()
|
||||
private lateinit var ledgerServices: MockServices
|
||||
|
||||
private val ledgerServices = MockServices(
|
||||
cordappPackages = listOf("net.corda.finance.contracts.asset"),
|
||||
initialIdentity = ALICE,
|
||||
identityService = rigorousMock<IdentityServiceInternal>().also {
|
||||
doReturn(ALICE_PARTY).whenever(it).partyFromKey(ALICE_PUBKEY)
|
||||
doReturn(BOB_PARTY).whenever(it).partyFromKey(BOB_PUBKEY)
|
||||
},
|
||||
networkParameters = testNetworkParameters(
|
||||
minimumPlatformVersion = 4,
|
||||
whitelistedContractImplementations = mapOf(
|
||||
Cash.PROGRAM_ID to listOf(SecureHash.zeroHash, SecureHash.allOnesHash),
|
||||
noPropagationContractClassName to listOf(SecureHash.zeroHash)
|
||||
),
|
||||
notaries = listOf(NotaryInfo(DUMMY_NOTARY, true))
|
||||
)
|
||||
)
|
||||
@Before
|
||||
fun setUp() {
|
||||
ledgerServices = MockServices(
|
||||
cordappPackages = listOf("net.corda.finance.contracts.asset"),
|
||||
initialIdentity = ALICE,
|
||||
identityService = rigorousMock<IdentityServiceInternal>().also {
|
||||
doReturn(ALICE_PARTY).whenever(it).partyFromKey(ALICE_PUBKEY)
|
||||
doReturn(BOB_PARTY).whenever(it).partyFromKey(BOB_PUBKEY)
|
||||
},
|
||||
networkParameters = testNetworkParameters(minimumPlatformVersion = 4)
|
||||
.copy(whitelistedContractImplementations = mapOf(
|
||||
Cash.PROGRAM_ID to listOf(SecureHash.zeroHash, SecureHash.allOnesHash),
|
||||
noPropagationContractClassName to listOf(SecureHash.zeroHash)),
|
||||
packageOwnership = mapOf("net.corda.finance.contracts.asset" to hashToSignatureConstraintsKey),
|
||||
notaries = listOf(NotaryInfo(DUMMY_NOTARY, true)))
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Happy path with the HashConstraint`() {
|
||||
@ -78,6 +105,45 @@ class ConstraintsPropagationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Happy path for Hash to Signature Constraint migration`() {
|
||||
val cordapps = (ledgerServices.cordappProvider as MockCordappProvider).cordapps
|
||||
val cordappAttachmentIds =
|
||||
cordapps.map { cordapp ->
|
||||
val unsignedAttId =
|
||||
cordapp.jarPath.toPath().inputStream().use { unsignedJarStream ->
|
||||
ledgerServices.attachments.importContractAttachment(cordapp.contractClassNames, "rpc", unsignedJarStream,null)
|
||||
}
|
||||
val jarAndSigner = ContractJarTestUtils.signContractJar(cordapp.jarPath, copyFirst = true, keyStoreDir = keyStoreDir.path)
|
||||
val signedJar = jarAndSigner.first
|
||||
val signedAttId =
|
||||
signedJar.inputStream().use { signedJarStream ->
|
||||
ledgerServices.attachments.importContractAttachment(cordapp.contractClassNames, "rpc", signedJarStream,null, listOf(jarAndSigner.second))
|
||||
}
|
||||
Pair(unsignedAttId, signedAttId)
|
||||
}
|
||||
|
||||
val unsignedAttachmentId = cordappAttachmentIds.first().first
|
||||
println("Unsigned: $unsignedAttachmentId")
|
||||
val signedAttachmentId = cordappAttachmentIds.first().second
|
||||
println("Signed: $signedAttachmentId")
|
||||
|
||||
ledgerServices.ledger(DUMMY_NOTARY) {
|
||||
unverifiedTransaction {
|
||||
attachment(Cash.PROGRAM_ID, unsignedAttachmentId)
|
||||
output(Cash.PROGRAM_ID, "c1", DUMMY_NOTARY, null, HashAttachmentConstraint(unsignedAttachmentId), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), ALICE_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Issue())
|
||||
}
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, signedAttachmentId)
|
||||
input("c1")
|
||||
output(Cash.PROGRAM_ID, "c2", DUMMY_NOTARY, null, SignatureAttachmentConstraint(hashToSignatureConstraintsKey), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Fail early in the TransactionBuilder when attempting to change the hash of the HashConstraint on the spending transaction`() {
|
||||
ledgerServices.ledger(DUMMY_NOTARY) {
|
||||
@ -164,9 +230,9 @@ class ConstraintsPropagationTests {
|
||||
|
||||
// the attachment is signed
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.allOnesHash, listOf(ALICE_PARTY.owningKey))
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.allOnesHash, listOf(hashToSignatureConstraintsKey))
|
||||
input("w1")
|
||||
output(Cash.PROGRAM_ID, "w2", DUMMY_NOTARY, null, SignatureAttachmentConstraint(ALICE_PUBKEY), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY))
|
||||
output(Cash.PROGRAM_ID, "w2", DUMMY_NOTARY, null, SignatureAttachmentConstraint(hashToSignatureConstraintsKey), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
verifies()
|
||||
}
|
||||
@ -223,10 +289,53 @@ class ConstraintsPropagationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Signature Constraints canBeTransitionedFrom Hash Constraints behaves as expected`() {
|
||||
|
||||
// unsigned attachment (for hash constraint)
|
||||
val attachmentUnsigned = mock<ContractAttachment>()
|
||||
val attachmentIdUnsigned = allOnesHash
|
||||
whenever(attachmentUnsigned.contract).thenReturn(propagatingContractClassName)
|
||||
|
||||
// signed attachment (for signature constraint)
|
||||
val attachmentSigned = mock<ContractAttachment>()
|
||||
val attachmentIdSigned = zeroHash
|
||||
whenever(attachmentSigned.signerKeys).thenReturn(listOf(ALICE_PARTY.owningKey))
|
||||
whenever(attachmentSigned.allContracts).thenReturn(setOf(propagatingContractClassName))
|
||||
|
||||
// network parameters
|
||||
val netParams = testNetworkParameters(minimumPlatformVersion = 4,
|
||||
packageOwnership = mapOf( "net.corda.core.contracts" to ALICE_PARTY.owningKey))
|
||||
|
||||
// attachment with context (both unsigned and signed attachments representing same contract)
|
||||
val attachmentWithContext = mock<AttachmentWithContext>()
|
||||
whenever(attachmentWithContext.contractAttachment).thenReturn(attachmentSigned)
|
||||
whenever(attachmentWithContext.contract).thenReturn(propagatingContractClassName)
|
||||
whenever(attachmentWithContext.networkParameters).thenReturn(netParams)
|
||||
|
||||
ledgerServices.attachments.importContractAttachment(attachmentIdSigned, attachmentSigned)
|
||||
ledgerServices.attachments.importContractAttachment(attachmentIdUnsigned, attachmentUnsigned)
|
||||
|
||||
// propagation check
|
||||
assertTrue(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(HashAttachmentConstraint(allOnesHash), attachmentWithContext))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Attachment canBeTransitionedFrom behaves as expected`() {
|
||||
|
||||
val attachment = mock<ContractAttachment>()
|
||||
// signed attachment (for signature constraint)
|
||||
val attachmentSigned = mock<ContractAttachment>()
|
||||
whenever(attachmentSigned.signerKeys).thenReturn(listOf(ALICE_PARTY.owningKey))
|
||||
whenever(attachmentSigned.allContracts).thenReturn(setOf(propagatingContractClassName))
|
||||
|
||||
// network parameters
|
||||
val netParams = testNetworkParameters(minimumPlatformVersion = 4,
|
||||
packageOwnership = mapOf(propagatingContractClassName to ALICE_PARTY.owningKey))
|
||||
|
||||
// attachment with context
|
||||
val attachment = mock<AttachmentWithContext>()
|
||||
whenever(attachment.networkParameters).thenReturn(netParams)
|
||||
whenever(attachment.contractAttachment).thenReturn(attachmentSigned)
|
||||
whenever(attachment.signerKeys).thenReturn(listOf(ALICE_PARTY.owningKey))
|
||||
|
||||
// Exhaustive positive check
|
||||
|
@ -89,7 +89,7 @@ class AttachmentTests : WithMockNet {
|
||||
val corruptBytes = "arggghhhh".toByteArray()
|
||||
System.arraycopy(corruptBytes, 0, attachment, 0, corruptBytes.size)
|
||||
|
||||
val corruptAttachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = attachment)
|
||||
val corruptAttachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = attachment, version = "1.0")
|
||||
badAliceNode.updateAttachment(corruptAttachment)
|
||||
|
||||
// Get n1 to fetch the attachment. Should receive corrupted bytes.
|
||||
|
@ -1,8 +1,6 @@
|
||||
package net.corda.node.internal
|
||||
package net.corda.core.node
|
||||
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.node.NotaryInfo
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.days
|
||||
@ -125,10 +123,10 @@ class NetworkParametersTest {
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(params.getOwnerOf("com.example.something.MyClass"), key1)
|
||||
assertEquals(params.getOwnerOf("com.examplesomething.MyClass"), null)
|
||||
assertEquals(params.getOwnerOf("com.examplestuff.something.MyClass"), key2)
|
||||
assertEquals(params.getOwnerOf("com.exam.something.MyClass"), null)
|
||||
assertEquals(params.getPackageOwnerOf("com.example.something.MyClass"), key1)
|
||||
assertEquals(params.getPackageOwnerOf("com.examplesomething.MyClass"), null)
|
||||
assertEquals(params.getPackageOwnerOf("com.examplestuff.something.MyClass"), key2)
|
||||
assertEquals(params.getPackageOwnerOf("com.exam.something.MyClass"), null)
|
||||
}
|
||||
|
||||
@Test
|
@ -5,6 +5,7 @@ import net.corda.core.contracts.Contract
|
||||
import net.corda.core.contracts.TransactionVerificationException
|
||||
import net.corda.core.internal.declaredField
|
||||
import net.corda.core.serialization.internal.AttachmentsClassLoader
|
||||
import net.corda.testing.core.internal.ContractJarTestUtils.signContractJar
|
||||
import net.corda.testing.internal.fakeAttachment
|
||||
import net.corda.testing.services.MockAttachmentStorage
|
||||
import org.apache.commons.io.IOUtils
|
||||
@ -19,6 +20,8 @@ class AttachmentsClassLoaderTests {
|
||||
|
||||
companion object {
|
||||
val ISOLATED_CONTRACTS_JAR_PATH: URL = AttachmentsClassLoaderTests::class.java.getResource("isolated.jar")
|
||||
val ISOLATED_CONTRACTS_JAR_PATH_V4: URL = AttachmentsClassLoaderTests::class.java.getResource("isolated-4.0.jar")
|
||||
val FINANCE_JAR_PATH: URL = AttachmentsClassLoaderTests::class.java.getResource("finance.jar")
|
||||
private const val ISOLATED_CONTRACT_CLASS_NAME = "net.corda.finance.contracts.isolated.AnotherDummyContract"
|
||||
|
||||
private fun readAttachment(attachment: Attachment, filepath: String): ByteArray {
|
||||
@ -48,6 +51,35 @@ class AttachmentsClassLoaderTests {
|
||||
assertEquals("helloworld", contract.declaredField<Any?>("magicString").value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test non-overlapping contract jar`() {
|
||||
val att1 = storage.importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
|
||||
val att2 = storage.importAttachment(ISOLATED_CONTRACTS_JAR_PATH_V4.openStream(), "app", "isolated-4.0.jar")
|
||||
|
||||
assertFailsWith(TransactionVerificationException.OverlappingAttachmentsException::class) {
|
||||
AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test valid overlapping contract jar`() {
|
||||
val isolatedId = storage.importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
|
||||
val signedJar = signContractJar(ISOLATED_CONTRACTS_JAR_PATH, copyFirst = true)
|
||||
val isolatedSignedId = storage.importAttachment(signedJar.first.toUri().toURL().openStream(), "app", "isolated-signed.jar")
|
||||
|
||||
// does not throw OverlappingAttachments exception
|
||||
AttachmentsClassLoader(arrayOf(isolatedId, isolatedSignedId).map { storage.openAttachment(it)!! })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test non-overlapping different contract jars`() {
|
||||
val att1 = storage.importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
|
||||
val att2 = storage.importAttachment(FINANCE_JAR_PATH.openStream(), "app", "finance.jar")
|
||||
|
||||
// does not throw OverlappingAttachments exception
|
||||
AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Load text resources from AttachmentsClassLoader`() {
|
||||
val att1 = storage.importAttachment(fakeAttachment("file1.txt", "some data").inputStream(), "app", "file1.jar")
|
||||
@ -62,13 +94,13 @@ class AttachmentsClassLoaderTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test overlapping file exception`() {
|
||||
val att1 = storage.importAttachment(fakeAttachment("file1.txt", "some data").inputStream(), "app", "file1.jar")
|
||||
val att2 = storage.importAttachment(fakeAttachment("file1.txt", "some other data").inputStream(), "app", "file2.jar")
|
||||
fun `Test valid overlapping file condition`() {
|
||||
val att1 = storage.importAttachment(fakeAttachment("file1.txt", "same data").inputStream(), "app", "file1.jar")
|
||||
val att2 = storage.importAttachment(fakeAttachment("file1.txt", "same data").inputStream(), "app", "file2.jar")
|
||||
|
||||
assertFailsWith(TransactionVerificationException.OverlappingAttachmentsException::class) {
|
||||
AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
|
||||
}
|
||||
val cl = AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
|
||||
val txt = IOUtils.toString(cl.getResourceAsStream("file1.txt"), Charsets.UTF_8.name())
|
||||
assertEquals("same data", txt)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -122,6 +122,7 @@ class TransactionTests {
|
||||
doReturn(SecureHash.zeroHash).whenever(it).id
|
||||
doReturn(fakeAttachment("nothing", "nada").inputStream()).whenever(it).open()
|
||||
}, DummyContract.PROGRAM_ID, uploader = "app"))
|
||||
attachments.first().openAsJAR()
|
||||
val id = SecureHash.randomSHA256()
|
||||
val timeWindow: TimeWindow? = null
|
||||
val privacySalt = PrivacySalt()
|
||||
|
BIN
core/src/test/resources/net/corda/core/transactions/finance.jar
Normal file
BIN
core/src/test/resources/net/corda/core/transactions/finance.jar
Normal file
Binary file not shown.
Binary file not shown.
@ -29,6 +29,11 @@ attachment JARs may not provide the same file path. If they do, the transaction
|
||||
state specifies both a constraint over attachments *and* a Contract class name to use, the specified class must appear
|
||||
in only one attachment.
|
||||
|
||||
.. note:: With the introduction of signature constraints in Corda 4, a new attachments classloader will verify that
|
||||
both signed and unsigned versions of an associated contract jar contain identical classes. This allows for automatic
|
||||
migration of hash-constrained states (created with pre-Corda 4 unsigned contract jars) to signature constrained states
|
||||
when used as outputs in new transactions using signed Corda 4 contract jars.
|
||||
|
||||
Recap: A corda transaction transitions input states to output states. Each state is composed of data, the name of the class that verifies the transition(contract), and
|
||||
the contract constraint. The transaction also contains a list of attachments (normal JARs) from where these classes will be loaded. There must be only one JAR containing each contract.
|
||||
The contract constraints are responsible to ensure the attachment JARs are following the rules set by the creators of the input states (in a continuous chain to the issue).
|
||||
@ -65,7 +70,7 @@ consumes notary and ledger resources, and is just in general more complex.
|
||||
Contract/State Agreement
|
||||
------------------------
|
||||
|
||||
Starting with Corda 4, ``ContractState``s must explicitly indicate which ``Contract`` they belong to. When a transaction is
|
||||
Starting with Corda 4, a ``ContractState`` must explicitly indicate which ``Contract`` it belongs to. When a transaction is
|
||||
verified, the contract bundled with each state in the transaction must be its "owning" contract, otherwise we cannot guarantee that
|
||||
the transition of the ``ContractState`` will be verified against the business rules that should apply to it.
|
||||
|
||||
@ -111,14 +116,19 @@ The other is to define the ``ContractState`` class as an inner class of the ``Co
|
||||
|
||||
If a ``ContractState``'s owning ``Contract`` cannot be identified by either of these mechanisms, and the ``targetVersion`` of the
|
||||
CorDapp is 4 or greater, then transaction verification will fail with a ``TransactionRequiredContractUnspecifiedException``. If
|
||||
the owning ``Contract`` _can_ be identified, but the ``ContractState`` has been bundled with a different contract, then
|
||||
the owning ``Contract`` *can* be identified, but the ``ContractState`` has been bundled with a different contract, then
|
||||
transaction verification will fail with a ``TransactionContractConflictException``.
|
||||
|
||||
How constraints work
|
||||
--------------------
|
||||
|
||||
Starting from Corda 3 there are two types of constraint that can be used: hash and zone whitelist. In future
|
||||
releases a third type will be added, the signature constraint.
|
||||
In Corda 4 there are three types of constraint that can be used in production environments: hash, zone whitelist and signature.
|
||||
For development purposes the ``AlwaysAcceptAttachmentConstraint`` allows any attachment to be selected.
|
||||
|
||||
Hash and zone whitelist constraints were available in Corda 3, with hash constraints being used as default.
|
||||
In Corda 4 the default constraint is the signature constraint if the jar is signed. Otherwise,
|
||||
the default constraint type is either a zone constraint, if the network parameters in effect when the
|
||||
transaction is built contain an entry for that contract class, or a hash constraint if not.
|
||||
|
||||
**Hash constraints.** The behaviour provided by public blockchain systems like Bitcoin and Ethereum is that once data is placed on the ledger,
|
||||
the program that controls it is fixed and cannot be changed. There is no support for upgrades at all. This implements a
|
||||
@ -143,20 +153,16 @@ parameters file, and trigger the network parameters upgrade process. This involv
|
||||
command to accept the new parameters file and then restarting the node. Node owners who do not restart their node in
|
||||
time effectively stop being a part of the network.
|
||||
|
||||
**Signature constraints.** These are not yet supported, but once implemented they will allow a state to require a JAR
|
||||
signed by a specified identity, via the regular Java ``jarsigner`` tool. This will be the most flexible type
|
||||
**Signature constraints.** These enforce an association between a state and its associated contract JAR which must be
|
||||
signed by a specified identity, via the regular Java ``jarsigner`` tool. This is the most flexible type
|
||||
and the smoothest to deploy: no restarts or contract upgrade transactions are needed.
|
||||
When CorDapp is build using :ref:`corda-gradle-plugin <cordapp_build_system_signing_cordapp_jar_ref>` the JAR is signed
|
||||
When a CorDapp is build using :ref:`corda-gradle-plugin <cordapp_build_system_signing_cordapp_jar_ref>` the JAR is signed
|
||||
by Corda development key by default, an external keystore can be configured or signing can be disabled.
|
||||
|
||||
.. warning:: CorDapps can only use signature constraints when participating in a Corda network using a minimum platform version of 4.
|
||||
An auto downgrade rule applies to signed CorDapps built and tested with Corda 4 but running on a Corda network of a lower version:
|
||||
if the associated contract class is whitelisted in the network parameters then zone constraints are applied, otherwise hash constraints are used.
|
||||
|
||||
**Defaults.** Currently, the default constraint type is either a zone constraint, if the network parameters in effect when the
|
||||
transaction is built contain an entry for that contract class, or a hash constraint if not. Once the Signature Constraints are introduced,
|
||||
the default constraint will be the Signature Constraint if the jar is signed.
|
||||
|
||||
A ``TransactionState`` has a ``constraint`` field that represents that state's attachment constraint. When a party
|
||||
constructs a ``TransactionState``, or adds a state using ``TransactionBuilder.addOutput(ContractState)`` without
|
||||
specifying the constraint parameter, a default value (``AutomaticPlaceholderConstraint``) is used. This default will be
|
||||
@ -234,7 +240,7 @@ As was mentioned above, the TransactionBuilder API gives the CorDapp developer o
|
||||
to construct output states with a constraint of his choosing.
|
||||
Also, as listed above, some constraints are more restrictive then others.
|
||||
For example, the ``HashAttachmentConstraint`` is the most restrictive, basically reducing the universe of possible attachments
|
||||
to 1, while the ``AlwaysAcceptAttachmentConstraint`` allows any attachment to be selected.
|
||||
to 1 (see migrating from hash constraints in note below), while the ``AlwaysAcceptAttachmentConstraint`` allows any attachment to be selected.
|
||||
|
||||
For the ledger to remain in a consistent state, the expected behavior is for output state to inherit the constraints of input states.
|
||||
This guarantees that for example, a transaction can't output a state with the ``AlwaysAcceptAttachmentConstraint`` when the
|
||||
@ -247,25 +253,35 @@ Starting with version 4 of Corda, the constraint propagation logic has been impl
|
||||
unless disabled using ``@NoConstraintPropagation`` - which reverts to the previous behavior.
|
||||
|
||||
For Contracts that are not annotated with ``@NoConstraintPropagation``, the platform implements a fairly simple constraint transition policy
|
||||
to ensure security and also allow the possibility to transition to the new SignatureAttachmentConstraint.
|
||||
to ensure security and also allow the possibility to transition to the new ``SignatureAttachmentConstraint``.
|
||||
|
||||
.. note:: Migration from hash to signature constraints is automatic if the transaction building node has a signed version of the
|
||||
original contract jar (used in previous transactions generating hash constrained states). Additionally, it is a requirement that
|
||||
the owner of this signed jar register the java package namespace of the encompassing contract classes with the network parameters.
|
||||
See :ref:`package_namespace_ownership` introduced in Corda 4.
|
||||
|
||||
During transaction building the ``AutomaticPlaceholderConstraint`` for output states will be resolved and the best contract attachment versions
|
||||
will be selected based on a variety of factors so that the above holds true.
|
||||
If it can't find attachments in storage or there are no possible constraints, the Transaction Builder will fail early.
|
||||
|
||||
For example:
|
||||
- In the simple case, if a ``MyContract`` input state is constrained by the ``HashAttachmentConstraint``, then the constraints of all output states of that type will be resolved
|
||||
to the ``HashAttachmentConstraint`` with the same hash, and the attachment with that hash will be selected.
|
||||
- For upgradeable constraints like the ``WhitelistedByZoneAttachmentConstraint``, the output states will inherit the same,
|
||||
and the selected attachment will be the latest version installed on the node.
|
||||
- A more complex case is when for ``MyContract``, one input state is constrained by the ``HashAttachmentConstraint``, while another
|
||||
state by the ``WhitelistedByZoneAttachmentConstraint``. To respect the rule from above, if the hash of the ``HashAttachmentConstraint``
|
||||
is whitelisted by the network, then the output states will inherit the ``HashAttachmentConstraint``, as it is more restrictive.
|
||||
If the hash was not whitelisted, then the builder will fail as it is unable to select a correct constraint.
|
||||
- The ``SignatureAttachmentConstraint`` is an upgradeable constraint, same as the ``WhitelistedByZoneAttachmentConstraint``.
|
||||
By convention we allow states to transition to the ``SignatureAttachmentConstraint`` from the ``WhitelistedByZoneAttachmentConstraint`` as long as the Signatures
|
||||
from new constraints are all the jarsigners from the whitelisted attachment.
|
||||
|
||||
- In the simple case, if a ``MyContract`` input state is constrained by the ``HashAttachmentConstraint``, then the constraints of all output states of that type will be resolved
|
||||
to the ``HashAttachmentConstraint`` with the same hash, and the attachment with that hash will be selected.
|
||||
|
||||
- For upgradeable constraints like the ``WhitelistedByZoneAttachmentConstraint``, the output states will inherit the same,
|
||||
and the selected attachment will be the latest version installed on the node.
|
||||
|
||||
- A more complex case is when for ``MyContract``, one input state is constrained by the ``HashAttachmentConstraint``, while another
|
||||
state by the ``WhitelistedByZoneAttachmentConstraint``. To respect the rule from above, if the hash of the ``HashAttachmentConstraint``
|
||||
is whitelisted by the network, then the output states will inherit the ``HashAttachmentConstraint``, as it is more restrictive.
|
||||
If the hash was not whitelisted, then the builder will fail as it is unable to select a correct constraint.
|
||||
|
||||
- The ``SignatureAttachmentConstraint`` is an upgradeable constraint, same as the ``WhitelistedByZoneAttachmentConstraint``.
|
||||
By convention we allow states to transition to the ``SignatureAttachmentConstraint`` from the ``WhitelistedByZoneAttachmentConstraint`` as long as the Signatures
|
||||
from new constraints are all the jarsigners from the whitelisted attachment. We also allow transitioning of states from ``HashAttachmentConstraint`` to
|
||||
``SignatureAttachmentConstraint`` where both the unsigned and signed versions of the associated contract attachment are loaded in a node, and the java
|
||||
package namespace of encompassing contract classes is registered with the network parameters using the same signing key as the signed contract jar.
|
||||
|
||||
For Contracts that are annotated with ``@NoConstraintPropagation``, the platform requires that the Transaction Builder specifies
|
||||
an actual constraint for the output states (the ``AutomaticPlaceholderConstraint`` can't be used) .
|
||||
|
@ -7,6 +7,10 @@ release, see :doc:`upgrade-notes`.
|
||||
Unreleased
|
||||
----------
|
||||
|
||||
* Automatic Constraints propagation for hash-constrained states to signature-constrained states.
|
||||
This allows Corda 4 signed CorDapps using signature constraints to consume existing hash constrained states generated
|
||||
by unsigned CorDapps in previous versions of Corda.
|
||||
|
||||
* ``SwapIdentitiesFlow``, from the experimental confidential-identities module, is now an inlined flow. Instead of passing in a ``Party`` with
|
||||
whom to exchange the anonymous identity, a ``FlowSession`` to that party is required instead. The flow running on the other side must
|
||||
also call ``SwapIdentitiesFlow``. This change was required as the previous API allowed any counterparty to generate anonoymous identities
|
||||
|
@ -319,6 +319,8 @@ An example configuration file:
|
||||
}
|
||||
]
|
||||
|
||||
.. _package_namespace_ownership:
|
||||
|
||||
Package namespace ownership
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
@ -0,0 +1,335 @@
|
||||
package net.corda.node
|
||||
|
||||
import net.corda.core.contracts.HashAttachmentConstraint
|
||||
import net.corda.core.contracts.SignatureAttachmentConstraint
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.withoutIssuer
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.messaging.vaultQueryBy
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.node.services.vault.QueryCriteria
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.finance.flows.CashIssueFlow
|
||||
import net.corda.finance.flows.CashPaymentFlow
|
||||
import net.corda.node.services.Permissions.Companion.invokeRpc
|
||||
import net.corda.node.services.Permissions.Companion.startFlow
|
||||
import net.corda.testing.common.internal.testNetworkParameters
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.testing.core.internal.JarSignatureTestUtils.generateKey
|
||||
import net.corda.testing.core.internal.SelfCleaningDir
|
||||
import net.corda.testing.driver.*
|
||||
import net.corda.testing.node.NotarySpec
|
||||
import net.corda.testing.node.User
|
||||
import net.corda.testing.node.internal.FINANCE_CORDAPP
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.junit.Test
|
||||
|
||||
class CordappConstraintsTests {
|
||||
|
||||
companion object {
|
||||
val user = User("u", "p", setOf(startFlow<CashIssueFlow>(), startFlow<CashPaymentFlow>(),
|
||||
invokeRpc(CordaRPCOps::wellKnownPartyFromX500Name),
|
||||
invokeRpc(CordaRPCOps::notaryIdentities),
|
||||
invokeRpc("vaultTrackByCriteria")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `issue cash using signature constraints`() {
|
||||
driver(DriverParameters(
|
||||
networkParameters = testNetworkParameters(minimumPlatformVersion = 4),
|
||||
inMemoryDB = false
|
||||
)) {
|
||||
|
||||
val alice = startNode(NodeParameters(additionalCordapps = listOf(FINANCE_CORDAPP.signJar()),
|
||||
providedName = ALICE_NAME,
|
||||
rpcUsers = listOf(user))).getOrThrow()
|
||||
|
||||
val expected = 500.DOLLARS
|
||||
val ref = OpaqueBytes.of(0x01)
|
||||
val issueTx = alice.rpc.startFlow(::CashIssueFlow, expected, ref, defaultNotaryIdentity).returnValue.getOrThrow()
|
||||
println("Issued transaction: $issueTx")
|
||||
|
||||
// Query vault
|
||||
val states = alice.rpc.vaultQueryBy<Cash.State>().states
|
||||
printVault(alice, states)
|
||||
|
||||
Assertions.assertThat(states).hasSize(1)
|
||||
Assertions.assertThat(states[0].state.constraint is SignatureAttachmentConstraint)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `issue cash using hash and signature constraints`() {
|
||||
driver(DriverParameters(
|
||||
networkParameters = testNetworkParameters(minimumPlatformVersion = 4),
|
||||
inMemoryDB = false
|
||||
)) {
|
||||
|
||||
println("Starting the node using unsigned contract jar ...")
|
||||
val alice = startNode(NodeParameters(providedName = ALICE_NAME,
|
||||
additionalCordapps = listOf(FINANCE_CORDAPP),
|
||||
rpcUsers = listOf(user))).getOrThrow()
|
||||
|
||||
val expected = 500.DOLLARS
|
||||
val ref = OpaqueBytes.of(0x01)
|
||||
val issueTx = alice.rpc.startFlow(::CashIssueFlow, expected, ref, defaultNotaryIdentity).returnValue.getOrThrow()
|
||||
println("Issued transaction: $issueTx")
|
||||
|
||||
// Query vault
|
||||
val states = alice.rpc.vaultQueryBy<Cash.State>().states
|
||||
printVault(alice, states)
|
||||
Assertions.assertThat(states).hasSize(1)
|
||||
|
||||
// Restart the node and re-query the vault
|
||||
println("Shutting down the node ...")
|
||||
(alice as OutOfProcess).process.destroyForcibly()
|
||||
alice.stop()
|
||||
|
||||
println("Restarting the node using signed contract jar ...")
|
||||
val restartedNode = startNode(NodeParameters(providedName = ALICE_NAME,
|
||||
additionalCordapps = listOf(FINANCE_CORDAPP.signJar()),
|
||||
regenerateCordappsOnStart = true
|
||||
)).getOrThrow()
|
||||
|
||||
val statesAfterRestart = restartedNode.rpc.vaultQueryBy<Cash.State>().states
|
||||
printVault(restartedNode, statesAfterRestart)
|
||||
Assertions.assertThat(statesAfterRestart).hasSize(1)
|
||||
|
||||
val issueTx2 = restartedNode.rpc.startFlow(::CashIssueFlow, expected, ref, defaultNotaryIdentity).returnValue.getOrThrow()
|
||||
println("Issued 2nd transaction: $issueTx2")
|
||||
|
||||
// Query vault
|
||||
val allStates = restartedNode.rpc.vaultQueryBy<Cash.State>().states
|
||||
printVault(restartedNode, allStates)
|
||||
|
||||
Assertions.assertThat(allStates).hasSize(2)
|
||||
Assertions.assertThat(allStates[0].state.constraint is HashAttachmentConstraint)
|
||||
Assertions.assertThat(allStates[1].state.constraint is SignatureAttachmentConstraint)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `issue and consume cash using hash constraints`() {
|
||||
driver(DriverParameters(cordappsForAllNodes = listOf(FINANCE_CORDAPP),
|
||||
extraCordappPackagesToScan = listOf("net.corda.finance"),
|
||||
networkParameters = testNetworkParameters(minimumPlatformVersion = 4),
|
||||
inMemoryDB = false)) {
|
||||
|
||||
val (alice, bob) = listOf(
|
||||
startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)),
|
||||
startNode(providedName = BOB_NAME, rpcUsers = listOf(user))
|
||||
).map { it.getOrThrow() }
|
||||
|
||||
// Issue Cash
|
||||
val issueTx = alice.rpc.startFlow(::CashIssueFlow, 1000.DOLLARS, OpaqueBytes.of(1), defaultNotaryIdentity).returnValue.getOrThrow()
|
||||
println("Issued transaction: $issueTx")
|
||||
|
||||
// Query vault
|
||||
val states = alice.rpc.vaultQueryBy<Cash.State>().states
|
||||
printVault(alice, states)
|
||||
|
||||
// Register for Bob vault updates
|
||||
val vaultUpdatesBob = bob.rpc.vaultTrackByCriteria(Cash.State::class.java, QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL)).updates
|
||||
|
||||
// Transfer Cash
|
||||
val bobParty = bob.rpc.wellKnownPartyFromX500Name(BOB_NAME)!!
|
||||
val transferTx = alice.rpc.startFlow(::CashPaymentFlow, 1000.DOLLARS, bobParty, true, defaultNotaryIdentity).returnValue.getOrThrow()
|
||||
println("Payment transaction: $transferTx")
|
||||
|
||||
// Query vaults
|
||||
val aliceQuery = alice.rpc.vaultQueryBy<Cash.State>(QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.CONSUMED))
|
||||
val aliceStates = aliceQuery.states
|
||||
printVault(alice, aliceQuery.states)
|
||||
|
||||
Assertions.assertThat(aliceStates).hasSize(1)
|
||||
Assertions.assertThat(aliceStates[0].state.data.amount.withoutIssuer()).isEqualTo(1000.DOLLARS)
|
||||
Assertions.assertThat(aliceQuery.statesMetadata[0].status).isEqualTo(Vault.StateStatus.CONSUMED)
|
||||
Assertions.assertThat(aliceQuery.statesMetadata[0].constraintInfo!!.type()).isEqualTo(Vault.ConstraintInfo.Type.HASH)
|
||||
|
||||
// Check Bob Vault Updates
|
||||
vaultUpdatesBob.expectEvents {
|
||||
sequence(
|
||||
// MOVE
|
||||
expect { (consumed, produced) ->
|
||||
require(consumed.isEmpty()) { consumed.size }
|
||||
require(produced.size == 1) { produced.size }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val bobQuery = bob.rpc.vaultQueryBy<Cash.State>()
|
||||
val bobStates = bobQuery.states
|
||||
printVault(bob, bobQuery.states)
|
||||
|
||||
Assertions.assertThat(bobStates).hasSize(1)
|
||||
Assertions.assertThat(bobStates[0].state.data.amount.withoutIssuer()).isEqualTo(1000.DOLLARS)
|
||||
Assertions.assertThat(bobQuery.statesMetadata[0].status).isEqualTo(Vault.StateStatus.UNCONSUMED)
|
||||
Assertions.assertThat(bobQuery.statesMetadata[0].constraintInfo!!.type()).isEqualTo(Vault.ConstraintInfo.Type.HASH)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `issue and consume cash using signature constraints`() {
|
||||
driver(DriverParameters(cordappsForAllNodes = listOf(FINANCE_CORDAPP.signJar()),
|
||||
extraCordappPackagesToScan = listOf("net.corda.finance"),
|
||||
networkParameters = testNetworkParameters(minimumPlatformVersion = 4),
|
||||
inMemoryDB = false)) {
|
||||
|
||||
val (alice, bob) = listOf(
|
||||
startNode(NodeParameters(providedName = ALICE_NAME, rpcUsers = listOf(user), additionalCordapps = listOf(FINANCE_CORDAPP.signJar()))),
|
||||
startNode(NodeParameters(providedName = BOB_NAME, rpcUsers = listOf(user), additionalCordapps = listOf(FINANCE_CORDAPP.signJar())))
|
||||
).map { it.getOrThrow() }
|
||||
|
||||
// Issue Cash
|
||||
val issueTx = alice.rpc.startFlow(::CashIssueFlow, 1000.DOLLARS, OpaqueBytes.of(1), defaultNotaryIdentity).returnValue.getOrThrow()
|
||||
println("Issued transaction: $issueTx")
|
||||
|
||||
// Query vault
|
||||
val states = alice.rpc.vaultQueryBy<Cash.State>().states
|
||||
printVault(alice, states)
|
||||
|
||||
// Register for Bob vault updates
|
||||
val vaultUpdatesBob = bob.rpc.vaultTrackByCriteria(Cash.State::class.java, QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL)).updates
|
||||
|
||||
// Transfer Cash
|
||||
val bobParty = bob.rpc.wellKnownPartyFromX500Name(BOB_NAME)!!
|
||||
val transferTx = alice.rpc.startFlow(::CashPaymentFlow, 1000.DOLLARS, bobParty).returnValue.getOrThrow()
|
||||
println("Payment transaction: $transferTx")
|
||||
|
||||
// Query vaults
|
||||
val aliceQuery = alice.rpc.vaultQueryBy<Cash.State>(QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.CONSUMED))
|
||||
val aliceStates = aliceQuery.states
|
||||
printVault(alice, aliceStates)
|
||||
|
||||
Assertions.assertThat(aliceStates).hasSize(1)
|
||||
Assertions.assertThat(aliceStates[0].state.data.amount.withoutIssuer()).isEqualTo(1000.DOLLARS)
|
||||
Assertions.assertThat(aliceQuery.statesMetadata[0].status).isEqualTo(Vault.StateStatus.CONSUMED)
|
||||
Assertions.assertThat(aliceQuery.statesMetadata[0].constraintInfo!!.type()).isEqualTo(Vault.ConstraintInfo.Type.SIGNATURE)
|
||||
|
||||
// Check Bob Vault Updates
|
||||
vaultUpdatesBob.expectEvents {
|
||||
sequence(
|
||||
// MOVE
|
||||
expect { (consumed, produced) ->
|
||||
require(consumed.isEmpty()) { consumed.size }
|
||||
require(produced.size == 1) { produced.size }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val bobQuery = bob.rpc.vaultQueryBy<Cash.State>()
|
||||
val bobStates = bobQuery.states
|
||||
printVault(bob, bobStates)
|
||||
|
||||
Assertions.assertThat(bobStates).hasSize(1)
|
||||
Assertions.assertThat(bobStates[0].state.data.amount.withoutIssuer()).isEqualTo(1000.DOLLARS)
|
||||
Assertions.assertThat(bobQuery.statesMetadata[0].status).isEqualTo(Vault.StateStatus.UNCONSUMED)
|
||||
Assertions.assertThat(bobQuery.statesMetadata[0].constraintInfo!!.type()).isEqualTo(Vault.ConstraintInfo.Type.SIGNATURE)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `issue cash and transfer using hash to signature constraints migration`() {
|
||||
|
||||
// signing key setup
|
||||
val keyStoreDir = SelfCleaningDir()
|
||||
val packageOwnerKey = keyStoreDir.path.generateKey()
|
||||
|
||||
driver(DriverParameters(cordappsForAllNodes = listOf(FINANCE_CORDAPP),
|
||||
notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = false)),
|
||||
extraCordappPackagesToScan = listOf("net.corda.finance"),
|
||||
networkParameters = testNetworkParameters(minimumPlatformVersion = 4,
|
||||
packageOwnership = mapOf("net.corda.finance.contracts.asset" to packageOwnerKey)),
|
||||
inMemoryDB = false)) {
|
||||
|
||||
val (alice, bob) = listOf(
|
||||
startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)),
|
||||
startNode(providedName = BOB_NAME, rpcUsers = listOf(user))
|
||||
).map { it.getOrThrow() }
|
||||
|
||||
// Issue Cash
|
||||
val issueTx = alice.rpc.startFlow(::CashIssueFlow, 1000.DOLLARS, OpaqueBytes.of(1), defaultNotaryIdentity).returnValue.getOrThrow()
|
||||
println("Issued transaction: $issueTx")
|
||||
|
||||
// Query vault
|
||||
val states = alice.rpc.vaultQueryBy<Cash.State>().states
|
||||
printVault(alice, states)
|
||||
|
||||
// Restart the node and re-query the vault
|
||||
println("Shutting down the node for $ALICE_NAME ...")
|
||||
(alice as OutOfProcess).process.destroyForcibly()
|
||||
alice.stop()
|
||||
|
||||
// Restart the node and re-query the vault
|
||||
println("Shutting down the node for $BOB_NAME ...")
|
||||
(bob as OutOfProcess).process.destroyForcibly()
|
||||
bob.stop()
|
||||
|
||||
println("Restarting the node for $ALICE_NAME ...")
|
||||
val restartedAlice = startNode(NodeParameters(providedName = ALICE_NAME,
|
||||
additionalCordapps = listOf(FINANCE_CORDAPP.signJar(keyStoreDir.path)),
|
||||
regenerateCordappsOnStart = true
|
||||
)).getOrThrow()
|
||||
|
||||
println("Restarting the node for $BOB_NAME ...")
|
||||
val restartedBob = startNode(NodeParameters(providedName = BOB_NAME,
|
||||
additionalCordapps = listOf(FINANCE_CORDAPP.signJar(keyStoreDir.path)),
|
||||
regenerateCordappsOnStart = true
|
||||
)).getOrThrow()
|
||||
|
||||
// Register for Bob vault updates
|
||||
val vaultUpdatesBob = restartedBob.rpc.vaultTrackByCriteria(Cash.State::class.java, QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL)).updates
|
||||
|
||||
// Transfer Cash
|
||||
val bobParty = restartedBob.rpc.wellKnownPartyFromX500Name(BOB_NAME)!!
|
||||
val transferTxn = restartedAlice.rpc.startFlow(::CashPaymentFlow, 1000.DOLLARS, bobParty, true, defaultNotaryIdentity).returnValue.getOrThrow()
|
||||
println("Payment transaction: $transferTxn")
|
||||
|
||||
// Query vault
|
||||
println("Vault query for ALICE after cash transfer ...")
|
||||
val aliceQuery = restartedAlice.rpc.vaultQueryBy<Cash.State>(QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.CONSUMED))
|
||||
val aliceStates = aliceQuery.states
|
||||
printVault(alice, aliceStates)
|
||||
|
||||
Assertions.assertThat(aliceStates).hasSize(1)
|
||||
Assertions.assertThat(aliceStates[0].state.data.amount.withoutIssuer()).isEqualTo(1000.DOLLARS)
|
||||
Assertions.assertThat(aliceQuery.statesMetadata[0].status).isEqualTo(Vault.StateStatus.CONSUMED)
|
||||
Assertions.assertThat(aliceQuery.statesMetadata[0].constraintInfo!!.type()).isEqualTo(Vault.ConstraintInfo.Type.HASH)
|
||||
|
||||
// Check Bob Vault Updates
|
||||
vaultUpdatesBob.expectEvents {
|
||||
sequence(
|
||||
// MOVE
|
||||
expect { (consumed, produced) ->
|
||||
require(consumed.isEmpty()) { consumed.size }
|
||||
require(produced.size == 1) { produced.size }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
println("Vault query for BOB after cash transfer ...")
|
||||
val bobQuery = restartedBob.rpc.vaultQueryBy<Cash.State>()
|
||||
val bobStates = bobQuery.states
|
||||
printVault(bob, bobStates)
|
||||
|
||||
Assertions.assertThat(bobStates).hasSize(1)
|
||||
Assertions.assertThat(bobStates[0].state.data.amount.withoutIssuer()).isEqualTo(1000.DOLLARS)
|
||||
Assertions.assertThat(bobQuery.statesMetadata[0].status).isEqualTo(Vault.StateStatus.UNCONSUMED)
|
||||
Assertions.assertThat(bobQuery.statesMetadata[0].constraintInfo!!.type()).isEqualTo(Vault.ConstraintInfo.Type.SIGNATURE)
|
||||
|
||||
// clean-up
|
||||
keyStoreDir.close()
|
||||
}
|
||||
}
|
||||
|
||||
private fun printVault(node: NodeHandle, states: List<StateAndRef<Cash.State>>) {
|
||||
println("Vault query for ${node.nodeInfo.singleIdentity()} returned ${states.size} records")
|
||||
states.forEach {
|
||||
println(it.state)
|
||||
}
|
||||
}
|
||||
}
|
@ -212,6 +212,7 @@ object DefaultKryoCustomizer {
|
||||
kryo.writeClassAndObject(output, obj.additionalContracts)
|
||||
output.writeString(obj.uploader)
|
||||
kryo.writeClassAndObject(output, obj.signerKeys)
|
||||
output.writeString(obj.version)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@ -222,6 +223,7 @@ object DefaultKryoCustomizer {
|
||||
val additionalContracts = kryo.readClassAndObject(input) as Set<ContractClassName>
|
||||
val uploader = input.readString()
|
||||
val signers = kryo.readClassAndObject(input) as List<PublicKey>
|
||||
val version = input.readString()
|
||||
val context = kryo.serializationContext()!!
|
||||
val attachmentStorage = context.serviceHub.attachments
|
||||
|
||||
@ -233,14 +235,15 @@ object DefaultKryoCustomizer {
|
||||
override val id = attachmentHash
|
||||
}
|
||||
|
||||
return ContractAttachment(lazyAttachment, contract, additionalContracts, uploader, signers)
|
||||
return ContractAttachment(lazyAttachment, contract, additionalContracts, uploader, signers, version)
|
||||
} else {
|
||||
val attachment = GeneratedAttachment(input.readBytesWithLength())
|
||||
val contract = input.readString()
|
||||
val additionalContracts = kryo.readClassAndObject(input) as Set<ContractClassName>
|
||||
val uploader = input.readString()
|
||||
val signers = kryo.readClassAndObject(input) as List<PublicKey>
|
||||
return ContractAttachment(attachment, contract, additionalContracts, uploader, signers)
|
||||
val version = input.readString()
|
||||
return ContractAttachment(attachment, contract, additionalContracts, uploader, signers, version)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ import java.nio.file.Paths
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import java.util.jar.Attributes.Name.IMPLEMENTATION_VERSION
|
||||
import java.util.jar.JarInputStream
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
import javax.persistence.*
|
||||
@ -106,7 +107,11 @@ class NodeAttachmentService(
|
||||
@Column(name = "signer", nullable = false)
|
||||
@CollectionTable(name = "${NODE_DATABASE_PREFIX}attachments_signers", joinColumns = [(JoinColumn(name = "att_id", referencedColumnName = "att_id"))],
|
||||
foreignKey = ForeignKey(name = "FK__signers__attachments"))
|
||||
var signers: List<PublicKey>? = null
|
||||
var signers: List<PublicKey>? = null,
|
||||
|
||||
// Assumption: only Contract Attachments are versioned.
|
||||
@Column(name = "version", nullable = true)
|
||||
var version: String? = null
|
||||
)
|
||||
|
||||
@VisibleForTesting
|
||||
@ -230,7 +235,7 @@ class NodeAttachmentService(
|
||||
val contracts = attachment.contractClassNames
|
||||
if (contracts != null && contracts.isNotEmpty()) {
|
||||
ContractAttachment(it, contracts.first(), contracts.drop(1).toSet(), attachment.uploader, attachment.signers?.toList()
|
||||
?: emptyList())
|
||||
?: emptyList(), attachment.version ?: UNKNOWN_VERSION)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
@ -313,6 +318,7 @@ class NodeAttachmentService(
|
||||
if (!hasAttachment(id)) {
|
||||
checkIsAValidJAR(bytes.inputStream())
|
||||
val jarSigners = getSigners(bytes)
|
||||
val contractVersion = getVersion(bytes)
|
||||
val session = currentDBSession()
|
||||
val attachment = NodeAttachmentService.DBAttachment(
|
||||
attId = id.toString(),
|
||||
@ -320,7 +326,8 @@ class NodeAttachmentService(
|
||||
uploader = uploader,
|
||||
filename = filename,
|
||||
contractClassNames = contractClassNames,
|
||||
signers = jarSigners
|
||||
signers = jarSigners,
|
||||
version = contractVersion
|
||||
)
|
||||
session.save(attachment)
|
||||
attachmentCount.inc()
|
||||
@ -347,6 +354,11 @@ class NodeAttachmentService(
|
||||
private fun getSigners(attachmentBytes: ByteArray) =
|
||||
JarSignatureCollector.collectSigners(JarInputStream(attachmentBytes.inputStream()))
|
||||
|
||||
private fun getVersion(attachmentBytes: ByteArray) =
|
||||
JarInputStream(attachmentBytes.inputStream()).use {
|
||||
it.manifest?.mainAttributes?.getValue(IMPLEMENTATION_VERSION) ?: "1.0"
|
||||
}
|
||||
|
||||
@Suppress("OverridingDeprecatedMember")
|
||||
override fun importOrGetAttachment(jar: InputStream): AttachmentId {
|
||||
return try {
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.node.services.vault
|
||||
|
||||
import net.corda.core.contracts.ContractClassName
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.identity.AbstractParty
|
||||
@ -26,6 +27,7 @@ import org.hibernate.query.criteria.internal.expression.LiteralExpression
|
||||
import org.hibernate.query.criteria.internal.path.SingularAttributePath
|
||||
import org.hibernate.query.criteria.internal.predicate.ComparisonPredicate
|
||||
import org.hibernate.query.criteria.internal.predicate.InPredicate
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import javax.persistence.Tuple
|
||||
@ -206,7 +208,37 @@ class HibernateAttachmentQueryCriteriaParser(override val criteriaBuilder: Crite
|
||||
}
|
||||
|
||||
criteria.uploadDateCondition?.let {
|
||||
predicateSet.add(columnPredicateToPredicate(root.get<Instant>("upload_date"), it))
|
||||
predicateSet.add(columnPredicateToPredicate(root.get<Instant>("insertionDate"), it))
|
||||
}
|
||||
|
||||
criteria.contractClassNamesCondition?.let {
|
||||
val contractClassNames =
|
||||
if (criteria.contractClassNamesCondition is EqualityComparison)
|
||||
(criteria.contractClassNamesCondition as EqualityComparison<List<ContractClassName>>).rightLiteral
|
||||
else emptyList()
|
||||
val joinDBAttachmentToContractClassNames = root.joinList<NodeAttachmentService.DBAttachment, ContractClassName>("contractClassNames")
|
||||
predicateSet.add(criteriaBuilder.and(joinDBAttachmentToContractClassNames.`in`(contractClassNames)))
|
||||
}
|
||||
|
||||
criteria.signersCondition?.let {
|
||||
val signers =
|
||||
if (criteria.signersCondition is EqualityComparison)
|
||||
(criteria.signersCondition as EqualityComparison<List<PublicKey>>).rightLiteral
|
||||
else emptyList()
|
||||
val joinDBAttachmentToSigners = root.joinList<NodeAttachmentService.DBAttachment, PublicKey>("signers")
|
||||
predicateSet.add(criteriaBuilder.and(joinDBAttachmentToSigners.`in`(signers)))
|
||||
}
|
||||
|
||||
criteria.isSignedCondition?.let { isSigned ->
|
||||
val joinDBAttachmentToSigners = root.joinList<NodeAttachmentService.DBAttachment, PublicKey>("signers")
|
||||
if (isSigned == Builder.equal(true))
|
||||
predicateSet.add(criteriaBuilder.and(joinDBAttachmentToSigners.isNotNull))
|
||||
else
|
||||
predicateSet.add(criteriaBuilder.and(joinDBAttachmentToSigners.isNull))
|
||||
}
|
||||
|
||||
criteria.versionCondition?.let {
|
||||
predicateSet.add(columnPredicateToPredicate(root.get<String>("version"), it))
|
||||
}
|
||||
|
||||
return predicateSet
|
||||
|
@ -13,5 +13,6 @@
|
||||
<include file="migration/node-core.changelog-v8.xml"/>
|
||||
<include file="migration/node-core.changelog-tx-mapping.xml"/>
|
||||
<include file="migration/node-core.changelog-v9.xml"/>
|
||||
<include file="migration/node-core.changelog-v10.xml"/>
|
||||
|
||||
</databaseChangeLog>
|
||||
|
@ -0,0 +1,14 @@
|
||||
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd"
|
||||
logicalFilePath="migration/node-services.changelog-init.xml">
|
||||
|
||||
<changeSet author="R3.Corda" id="add_version_column">
|
||||
<addColumn tableName="node_attachments">
|
||||
<column name="version" type="NVARCHAR(64)"/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
@ -1,14 +1,17 @@
|
||||
package net.corda.node.services.vault;
|
||||
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import kotlin.Pair;
|
||||
import kotlin.Triple;
|
||||
import net.corda.core.contracts.*;
|
||||
import net.corda.core.crypto.CryptoUtils;
|
||||
import net.corda.core.crypto.SecureHash;
|
||||
import net.corda.core.identity.AbstractParty;
|
||||
import net.corda.core.identity.CordaX500Name;
|
||||
import net.corda.core.identity.Party;
|
||||
import net.corda.core.messaging.DataFeed;
|
||||
import net.corda.core.node.services.AttachmentStorage;
|
||||
import net.corda.core.node.services.IdentityService;
|
||||
import net.corda.core.node.services.Vault;
|
||||
import net.corda.core.node.services.VaultService;
|
||||
@ -16,15 +19,20 @@ import net.corda.core.node.services.vault.*;
|
||||
import net.corda.core.node.services.vault.QueryCriteria.LinearStateQueryCriteria;
|
||||
import net.corda.core.node.services.vault.QueryCriteria.VaultCustomQueryCriteria;
|
||||
import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria;
|
||||
import net.corda.core.node.services.vault.AttachmentQueryCriteria.AttachmentsQueryCriteria;
|
||||
import net.corda.finance.contracts.DealState;
|
||||
import net.corda.finance.contracts.asset.Cash;
|
||||
import net.corda.finance.schemas.CashSchemaV1;
|
||||
import net.corda.finance.schemas.test.SampleCashSchemaV2;
|
||||
import net.corda.node.services.api.IdentityServiceInternal;
|
||||
import net.corda.node.services.persistence.NodeAttachmentService;
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence;
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseTransaction;
|
||||
import net.corda.testing.core.internal.ContractJarTestUtils;
|
||||
import net.corda.testing.core.internal.SelfCleaningDir;
|
||||
import net.corda.testing.core.SerializationEnvironmentRule;
|
||||
import net.corda.testing.core.TestIdentity;
|
||||
import net.corda.testing.internal.TestingNamedCacheFactory;
|
||||
import net.corda.testing.internal.vault.DummyLinearContract;
|
||||
import net.corda.testing.internal.vault.VaultFiller;
|
||||
import net.corda.testing.node.MockServices;
|
||||
@ -33,6 +41,11 @@ import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.PublicKey;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
@ -41,9 +54,11 @@ import java.util.stream.StreamSupport;
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.Collections.emptyList;
|
||||
import static java.util.Collections.singletonList;
|
||||
import static net.corda.core.node.services.vault.Builder.equal;
|
||||
import static net.corda.core.node.services.vault.Builder.sum;
|
||||
import static net.corda.core.node.services.vault.QueryCriteriaUtils.*;
|
||||
import static net.corda.core.utilities.ByteArrays.toHexString;
|
||||
import static net.corda.testing.core.internal.ContractJarTestUtils.INSTANCE;
|
||||
import static net.corda.testing.core.TestConstants.*;
|
||||
import static net.corda.testing.internal.RigorousMockKt.rigorousMock;
|
||||
import static net.corda.testing.node.MockServices.makeTestDatabaseAndMockServices;
|
||||
@ -62,6 +77,7 @@ public class VaultQueryJavaTests {
|
||||
private VaultFiller vaultFiller;
|
||||
private MockServices issuerServices;
|
||||
private VaultService vaultService;
|
||||
private AttachmentStorage storage;
|
||||
private CordaPersistence database;
|
||||
|
||||
@Before
|
||||
@ -78,6 +94,7 @@ public class VaultQueryJavaTests {
|
||||
MockServices services = databaseAndServices.getSecond();
|
||||
vaultFiller = new VaultFiller(services, DUMMY_NOTARY);
|
||||
vaultService = services.getVaultService();
|
||||
storage = new NodeAttachmentService(new MetricRegistry(), new TestingNamedCacheFactory(100), database);
|
||||
}
|
||||
|
||||
@After
|
||||
@ -541,4 +558,75 @@ public class VaultQueryJavaTests {
|
||||
return tx;
|
||||
});
|
||||
}
|
||||
|
||||
private Pair<Path, SecureHash> makeTestJar(Path path) throws IOException {
|
||||
Path file = Paths.get(path.toAbsolutePath().toString(), "$counter.jar");
|
||||
ContractJarTestUtils.INSTANCE.makeTestJar(Files.newOutputStream(file));
|
||||
return new Pair(file, SecureHash.sha256(Files.readAllBytes(file)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAttachmentQueryCriteria() throws IOException, NoSuchFieldException {
|
||||
SelfCleaningDir selfCleaningDir = new SelfCleaningDir();
|
||||
Path path = selfCleaningDir.getPath();
|
||||
|
||||
Pair<Path, SecureHash> sampleJarAndHash = makeTestJar(path);
|
||||
Path sampleJar = sampleJarAndHash.component1();
|
||||
Path contractJar = INSTANCE.makeTestContractJar(path, "com.example.MyContract");
|
||||
|
||||
Pair<Path, PublicKey> signedContractJarAndKey = INSTANCE.makeTestSignedContractJar(path, "com.example.MyContract");
|
||||
Path signedContractJar = signedContractJarAndKey.component1();
|
||||
PublicKey publicKey = signedContractJarAndKey.component2();
|
||||
|
||||
Pair<Path, PublicKey> anotherSignedContractJarAndKey = INSTANCE.makeTestSignedContractJar(path, "com.example.AnotherContract");
|
||||
Path anotherSignedContractJar = anotherSignedContractJarAndKey.component1();
|
||||
|
||||
Path contractJarV2 = INSTANCE.makeTestContractJar(path, "com.example.MyContract", false, "2.0");
|
||||
Pair<Path, PublicKey> signedContractJarAndKeyV2 = INSTANCE.makeTestSignedContractJar(path, "com.example.MyContract", "2.0");
|
||||
Path signedContractJarV2 = signedContractJarAndKeyV2.component1();
|
||||
|
||||
storage.importAttachment(Files.newInputStream(sampleJar),"uploaderA", "sample.jar");
|
||||
storage.importAttachment(Files.newInputStream(contractJar), "uploaderB", "contract.jar");
|
||||
storage.importAttachment(Files.newInputStream(signedContractJar), "uploaderC", "contract-signed.jar");
|
||||
storage.importAttachment(Files.newInputStream(anotherSignedContractJar), "uploaderD", "another-contract-signed.jar");
|
||||
storage.importAttachment(Files.newInputStream(contractJarV2), "uploaderB", "contract-V2.jar");
|
||||
storage.importAttachment(Files.newInputStream(signedContractJarV2), "uploaderC", "contract-signed-V2.jar");
|
||||
|
||||
// contract class name
|
||||
FieldInfo contractClassNames = getField("contractClassNames", NodeAttachmentService.DBAttachment.class);
|
||||
ColumnPredicate<List<String>> contractClassNamesPredicate = equal(contractClassNames, singletonList("com.example.MyContract")).component2();
|
||||
|
||||
AttachmentsQueryCriteria criteria1 = new AttachmentsQueryCriteria().withContractClassNames(contractClassNamesPredicate);
|
||||
criteria1.withContractClassNames(contractClassNamesPredicate);
|
||||
assertThat(storage.queryAttachments(criteria1).size()).isEqualTo(4);
|
||||
|
||||
// signers
|
||||
FieldInfo signers = getField("signers", NodeAttachmentService.DBAttachment.class);
|
||||
ColumnPredicate<List<PublicKey>> signersPredicate = equal(signers, singletonList(publicKey)).component2();
|
||||
|
||||
AttachmentsQueryCriteria criteria2 = new AttachmentsQueryCriteria().withSigners(signersPredicate);
|
||||
assertThat(storage.queryAttachments(criteria2).size()).isEqualTo(1);
|
||||
|
||||
// isSigned
|
||||
FieldInfo isSigned = getField("signers", NodeAttachmentService.DBAttachment.class);
|
||||
ColumnPredicate<Boolean> isSignedPredicate = equal(isSigned, true).component2();
|
||||
|
||||
AttachmentsQueryCriteria criteria3 = new AttachmentsQueryCriteria().isSigned(isSignedPredicate);
|
||||
assertThat(storage.queryAttachments(criteria3).size()).isEqualTo(3);
|
||||
|
||||
// version
|
||||
FieldInfo version = getField("version", NodeAttachmentService.DBAttachment.class);
|
||||
ColumnPredicate<List<String>> version2Predicate = equal(version, asList("2.0")).component2();
|
||||
|
||||
AttachmentsQueryCriteria criteria4 = new AttachmentsQueryCriteria().withContractClassNames(contractClassNamesPredicate).isSigned(isSignedPredicate).withVersions(version2Predicate);
|
||||
assertThat(storage.queryAttachments(criteria4).size()).isEqualTo(1);
|
||||
|
||||
ColumnPredicate<List<String>> version1Predicate = equal(version, asList("1.0")).component2();
|
||||
ColumnPredicate<List<String>> manyContractClassNamesPredicate = equal(contractClassNames, asList("com.example.MyContract", "com.example.AnotherContract")).component2();
|
||||
|
||||
AttachmentsQueryCriteria criteria5 = new AttachmentsQueryCriteria().withContractClassNames(manyContractClassNamesPredicate).isSigned(isSignedPredicate).withVersions(version1Predicate);
|
||||
assertThat(storage.queryAttachments(criteria5).size()).isEqualTo(2);
|
||||
|
||||
selfCleaningDir.close();
|
||||
}
|
||||
}
|
||||
|
@ -4,13 +4,17 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import com.codahale.metrics.MetricRegistry
|
||||
import com.google.common.jimfs.Configuration
|
||||
import com.google.common.jimfs.Jimfs
|
||||
import net.corda.testing.core.internal.ContractJarTestUtils.makeTestContractJar
|
||||
import net.corda.testing.core.internal.ContractJarTestUtils.makeTestJar
|
||||
import net.corda.testing.core.internal.ContractJarTestUtils.makeTestSignedContractJar
|
||||
import net.corda.testing.core.internal.SelfCleaningDir
|
||||
import net.corda.core.contracts.ContractAttachment
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.node.services.vault.AttachmentQueryCriteria
|
||||
import net.corda.core.node.services.vault.AttachmentQueryCriteria.AttachmentsQueryCriteria
|
||||
import net.corda.core.node.services.vault.AttachmentSort
|
||||
import net.corda.core.node.services.vault.Builder
|
||||
import net.corda.core.node.services.vault.Sort
|
||||
@ -18,10 +22,6 @@ import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.services.transactions.PersistentUniquenessProvider
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.JarSignatureTestUtils.createJar
|
||||
import net.corda.testing.core.JarSignatureTestUtils.generateKey
|
||||
import net.corda.testing.core.JarSignatureTestUtils.signJar
|
||||
import net.corda.testing.internal.LogHelper
|
||||
import net.corda.testing.internal.TestingNamedCacheFactory
|
||||
import net.corda.testing.internal.configureDatabase
|
||||
@ -35,18 +35,10 @@ import org.junit.Before
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.Closeable
|
||||
import java.io.OutputStream
|
||||
import java.net.URI
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.*
|
||||
import java.security.PublicKey
|
||||
import java.util.jar.JarEntry
|
||||
import java.util.jar.JarOutputStream
|
||||
import javax.tools.JavaFileObject
|
||||
import javax.tools.SimpleJavaFileObject
|
||||
import javax.tools.StandardLocation
|
||||
import javax.tools.ToolProvider
|
||||
import java.nio.file.FileAlreadyExistsException
|
||||
import java.nio.file.FileSystem
|
||||
import java.nio.file.Path
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertNotEquals
|
||||
@ -201,23 +193,73 @@ class NodeAttachmentServiceTest {
|
||||
|
||||
assertEquals(
|
||||
listOf(hashB),
|
||||
storage.queryAttachments(AttachmentQueryCriteria.AttachmentsQueryCriteria(Builder.equal("uploaderB")))
|
||||
storage.queryAttachments(AttachmentsQueryCriteria(Builder.equal("uploaderB")))
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
listOf(hashB, hashC),
|
||||
storage.queryAttachments(AttachmentQueryCriteria.AttachmentsQueryCriteria(Builder.like("%uploader%")))
|
||||
storage.queryAttachments(AttachmentsQueryCriteria(Builder.like("%uploader%")))
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `contract class, versioning and signing metadata can be used to search`() {
|
||||
SelfCleaningDir().use { file ->
|
||||
val (sampleJar, _) = makeTestJar()
|
||||
val contractJar = makeTestContractJar(file.path, "com.example.MyContract")
|
||||
val (signedContractJar, publicKey) = makeTestSignedContractJar(file.path, "com.example.MyContract")
|
||||
val (anotherSignedContractJar, _) = makeTestSignedContractJar(file.path,"com.example.AnotherContract")
|
||||
val contractJarV2 = makeTestContractJar(file.path,"com.example.MyContract", version = "2.0")
|
||||
val (signedContractJarV2, publicKeyV2) = makeTestSignedContractJar(file.path,"com.example.MyContract", version = "2.0")
|
||||
|
||||
sampleJar.read { storage.importAttachment(it, "uploaderA", "sample.jar") }
|
||||
contractJar.read { storage.importAttachment(it, "uploaderB", "contract.jar") }
|
||||
signedContractJar.read { storage.importAttachment(it, "uploaderC", "contract-signed.jar") }
|
||||
anotherSignedContractJar.read { storage.importAttachment(it, "uploaderD", "another-contract-signed.jar") }
|
||||
contractJarV2.read { storage.importAttachment(it, "uploaderB", "contract-V2.jar") }
|
||||
signedContractJarV2.read { storage.importAttachment(it, "uploaderC", "contract-signed-V2.jar") }
|
||||
|
||||
assertEquals(
|
||||
4,
|
||||
storage.queryAttachments(AttachmentsQueryCriteria(contractClassNamesCondition = Builder.equal(listOf("com.example.MyContract")))).size
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
1,
|
||||
storage.queryAttachments(AttachmentsQueryCriteria(signersCondition = Builder.equal(listOf(publicKey)))).size
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
3,
|
||||
storage.queryAttachments(AttachmentsQueryCriteria(isSignedCondition = Builder.equal(true))).size
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
1,
|
||||
storage.queryAttachments(AttachmentsQueryCriteria(
|
||||
contractClassNamesCondition = Builder.equal(listOf("com.example.MyContract")),
|
||||
versionCondition = Builder.equal(listOf("2.0")),
|
||||
isSignedCondition = Builder.equal(true))).size
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
2,
|
||||
storage.queryAttachments(AttachmentsQueryCriteria(
|
||||
contractClassNamesCondition = Builder.equal(listOf("com.example.MyContract", "com.example.AnotherContract")),
|
||||
versionCondition = Builder.equal(listOf("1.0")),
|
||||
isSignedCondition = Builder.equal(true))).size
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sorting and compound conditions work`() {
|
||||
val (jarA, hashA) = makeTestJar(listOf(Pair("a", "a")))
|
||||
val (jarB, hashB) = makeTestJar(listOf(Pair("b", "b")))
|
||||
val (jarC, hashC) = makeTestJar(listOf(Pair("c", "c")))
|
||||
|
||||
fun uploaderCondition(s: String) = AttachmentQueryCriteria.AttachmentsQueryCriteria(uploaderCondition = Builder.equal(s))
|
||||
fun filenamerCondition(s: String) = AttachmentQueryCriteria.AttachmentsQueryCriteria(filenameCondition = Builder.equal(s))
|
||||
fun uploaderCondition(s: String) = AttachmentsQueryCriteria(uploaderCondition = Builder.equal(s))
|
||||
fun filenamerCondition(s: String) = AttachmentsQueryCriteria(filenameCondition = Builder.equal(s))
|
||||
|
||||
fun filenameSort(direction: Sort.Direction) = AttachmentSort(listOf(AttachmentSort.AttachmentSortColumn(AttachmentSort.AttachmentSortAttribute.FILENAME, direction)))
|
||||
|
||||
@ -230,16 +272,15 @@ class NodeAttachmentServiceTest {
|
||||
assertEquals(
|
||||
emptyList(),
|
||||
storage.queryAttachments(
|
||||
AttachmentQueryCriteria.AttachmentsQueryCriteria(uploaderCondition = Builder.equal("complexA"))
|
||||
.and(AttachmentQueryCriteria.AttachmentsQueryCriteria(uploaderCondition = Builder.equal("complexB"))))
|
||||
AttachmentsQueryCriteria(uploaderCondition = Builder.equal("complexA"))
|
||||
.and(AttachmentsQueryCriteria(uploaderCondition = Builder.equal("complexB"))))
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
listOf(hashA, hashB),
|
||||
storage.queryAttachments(
|
||||
|
||||
AttachmentQueryCriteria.AttachmentsQueryCriteria(uploaderCondition = Builder.equal("complexA"))
|
||||
.or(AttachmentQueryCriteria.AttachmentsQueryCriteria(uploaderCondition = Builder.equal("complexB"))))
|
||||
AttachmentsQueryCriteria(uploaderCondition = Builder.equal("complexA"))
|
||||
.or(AttachmentsQueryCriteria(uploaderCondition = Builder.equal("complexB"))))
|
||||
)
|
||||
|
||||
val complexCondition =
|
||||
@ -281,7 +322,7 @@ class NodeAttachmentServiceTest {
|
||||
val bytes = testJar.readAll()
|
||||
val corruptBytes = "arggghhhh".toByteArray()
|
||||
System.arraycopy(corruptBytes, 0, bytes, 0, corruptBytes.size)
|
||||
val corruptAttachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = bytes)
|
||||
val corruptAttachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = bytes, version = "1.0")
|
||||
session.merge(corruptAttachment)
|
||||
id
|
||||
}
|
||||
@ -354,74 +395,4 @@ class NodeAttachmentServiceTest {
|
||||
makeTestJar(file.outputStream(), extraEntries)
|
||||
return Pair(file, file.readAll().sha256())
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to create an automatically delete a temporary directory.
|
||||
*/
|
||||
class SelfCleaningDir : Closeable {
|
||||
val path: Path = Files.createTempDirectory(NodeAttachmentServiceTest::class.simpleName)
|
||||
override fun close() {
|
||||
path.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun makeTestJar(output: OutputStream, extraEntries: List<Pair<String, String>> = emptyList()) {
|
||||
output.use {
|
||||
val jar = JarOutputStream(it)
|
||||
jar.putNextEntry(JarEntry("test1.txt"))
|
||||
jar.write("This is some useful content".toByteArray())
|
||||
jar.closeEntry()
|
||||
jar.putNextEntry(JarEntry("test2.txt"))
|
||||
jar.write("Some more useful content".toByteArray())
|
||||
extraEntries.forEach { entry ->
|
||||
jar.putNextEntry(JarEntry(entry.first))
|
||||
jar.write(entry.second.toByteArray())
|
||||
}
|
||||
jar.closeEntry()
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeTestSignedContractJar(workingDir: Path, contractName: String): Pair<Path, PublicKey> {
|
||||
val alias = "testAlias"
|
||||
val pwd = "testPassword"
|
||||
workingDir.generateKey(alias, pwd, ALICE_NAME.toString())
|
||||
val jarName = makeTestContractJar(workingDir, contractName)
|
||||
val signer = workingDir.signJar(jarName, alias, pwd)
|
||||
return workingDir.resolve(jarName) to signer
|
||||
}
|
||||
|
||||
private fun makeTestContractJar(workingDir: Path, contractName: String): String {
|
||||
val packages = contractName.split(".")
|
||||
val jarName = "testattachment.jar"
|
||||
val className = packages.last()
|
||||
createTestClass(workingDir, className, packages.subList(0, packages.size - 1))
|
||||
workingDir.createJar(jarName, "${contractName.replace(".", "/")}.class")
|
||||
return jarName
|
||||
}
|
||||
|
||||
private fun createTestClass(workingDir: Path, className: String, packages: List<String>): Path {
|
||||
val newClass = """package ${packages.joinToString(".")};
|
||||
import net.corda.core.contracts.*;
|
||||
import net.corda.core.transactions.*;
|
||||
|
||||
public class $className implements Contract {
|
||||
@Override
|
||||
public void verify(LedgerTransaction tx) throws IllegalArgumentException {
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val compiler = ToolProvider.getSystemJavaCompiler()
|
||||
val source = object : SimpleJavaFileObject(URI.create("string:///${packages.joinToString("/")}/$className.java"), JavaFileObject.Kind.SOURCE) {
|
||||
override fun getCharContent(ignoreEncodingErrors: Boolean): CharSequence {
|
||||
return newClass
|
||||
}
|
||||
}
|
||||
val fileManager = compiler.getStandardFileManager(null, null, null)
|
||||
fileManager.setLocation(StandardLocation.CLASS_OUTPUT, listOf(workingDir.toFile()))
|
||||
|
||||
compiler.getTask(System.out.writer(), fileManager, null, null, null, listOf(source)).call()
|
||||
return Paths.get(fileManager.list(StandardLocation.CLASS_OUTPUT, "", setOf(JavaFileObject.Kind.CLASS), true).single().name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,13 +24,13 @@ class ContractAttachmentSerializer(factory: SerializerFactory) : CustomSerialize
|
||||
} catch (e: Exception) {
|
||||
throw MissingAttachmentsException(listOf(obj.id))
|
||||
}
|
||||
return ContractAttachmentProxy(GeneratedAttachment(bytes), obj.contract, obj.additionalContracts, obj.uploader, obj.signerKeys)
|
||||
return ContractAttachmentProxy(GeneratedAttachment(bytes), obj.contract, obj.additionalContracts, obj.uploader, obj.signerKeys, obj.version)
|
||||
}
|
||||
|
||||
override fun fromProxy(proxy: ContractAttachmentProxy): ContractAttachment {
|
||||
return ContractAttachment(proxy.attachment, proxy.contract, proxy.contracts, proxy.uploader, proxy.signers)
|
||||
return ContractAttachment(proxy.attachment, proxy.contract, proxy.contracts, proxy.uploader, proxy.signers, proxy.version)
|
||||
}
|
||||
|
||||
@KeepForDJVM
|
||||
data class ContractAttachmentProxy(val attachment: Attachment, val contract: ContractClassName, val contracts: Set<ContractClassName>, val uploader: String?, val signers: List<PublicKey>)
|
||||
data class ContractAttachmentProxy(val attachment: Attachment, val contract: ContractClassName, val contracts: Set<ContractClassName>, val uploader: String?, val signers: List<PublicKey>, val version: String)
|
||||
}
|
@ -4,6 +4,7 @@ import net.corda.core.DoNotImplement
|
||||
import net.corda.core.internal.PLATFORM_VERSION
|
||||
import net.corda.testing.node.internal.TestCordappImpl
|
||||
import net.corda.testing.node.internal.simplifyScanPackages
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* Represents information about a CorDapp. Used to generate CorDapp JARs in tests.
|
||||
@ -31,6 +32,9 @@ interface TestCordapp {
|
||||
/** Returns the set of package names scanned for this test CorDapp. */
|
||||
val packages: Set<String>
|
||||
|
||||
/** Returns whether the CorDapp should be jar signed. */
|
||||
val signJar: Boolean
|
||||
|
||||
/** Return a copy of this [TestCordapp] but with the specified name. */
|
||||
fun withName(name: String): TestCordapp
|
||||
|
||||
@ -49,6 +53,10 @@ interface TestCordapp {
|
||||
/** Returns a copy of this [TestCordapp] but with the specified CorDapp config. */
|
||||
fun withConfig(config: Map<String, Any>): TestCordapp
|
||||
|
||||
/** Returns a signed copy of this [TestCordapp].
|
||||
* Optionally can pass in the location of an existing java key store to use */
|
||||
fun signJar(keyStorePath: Path? = null): TestCordappImpl
|
||||
|
||||
class Factory {
|
||||
companion object {
|
||||
/**
|
||||
|
@ -5,8 +5,8 @@ import net.corda.core.crypto.sha256
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.testing.core.JarSignatureTestUtils.signJar
|
||||
import net.corda.testing.core.JarSignatureTestUtils.generateKey
|
||||
import net.corda.testing.core.internal.JarSignatureTestUtils.generateKey
|
||||
import net.corda.testing.core.internal.JarSignatureTestUtils.signJar
|
||||
import net.corda.testing.node.TestCordapp
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
@ -37,17 +37,18 @@ object TestCordappDirectories {
|
||||
val configDir = (cordappDir / "config").createDirectories()
|
||||
val jarFile = cordappDir / "$filename.jar"
|
||||
cordapp.packageAsJar(jarFile)
|
||||
//TODO in future we may extend the signing with user-defined key-stores/certs/keys.
|
||||
if (signJar) {
|
||||
if (signJar || cordapp.signJar) {
|
||||
val testKeystore = "_teststore"
|
||||
val alias = "Test"
|
||||
val pwd = "secret!"
|
||||
if (!(cordappsDirectory / testKeystore).exists()) {
|
||||
if (!(cordappsDirectory / testKeystore).exists() && (cordapp.keyStorePath == null)) {
|
||||
cordappsDirectory.generateKey(alias, pwd, "O=Test Company Ltd,OU=Test,L=London,C=GB")
|
||||
}
|
||||
(cordappsDirectory / testKeystore).copyTo(cordappDir / testKeystore)
|
||||
cordappDir.signJar("$filename.jar", alias, pwd)
|
||||
}
|
||||
val keyStorePathToUse = cordapp.keyStorePath ?: cordappsDirectory
|
||||
(keyStorePathToUse / testKeystore).copyTo(cordappDir / testKeystore)
|
||||
val pk = cordappDir.signJar("$filename.jar", alias, pwd)
|
||||
logger.debug { "Signed Jar: $cordappDir/$filename.jar with public key $pk" }
|
||||
} else logger.debug { "Unsigned Jar: $cordappDir/$filename.jar" }
|
||||
(configDir / "$filename.conf").writeText(configString)
|
||||
logger.debug { "$cordapp packaged into $jarFile" }
|
||||
cordappDir
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.testing.node.internal
|
||||
|
||||
import net.corda.testing.node.TestCordapp
|
||||
import java.nio.file.Path
|
||||
|
||||
data class TestCordappImpl(override val name: String,
|
||||
override val version: String,
|
||||
@ -9,7 +10,10 @@ data class TestCordappImpl(override val name: String,
|
||||
override val targetVersion: Int,
|
||||
override val config: Map<String, Any>,
|
||||
override val packages: Set<String>,
|
||||
val classes: Set<Class<*>>) : TestCordapp {
|
||||
override val signJar: Boolean = false,
|
||||
val keyStorePath: Path? = null,
|
||||
val classes: Set<Class<*>>
|
||||
) : TestCordapp {
|
||||
|
||||
override fun withName(name: String): TestCordappImpl = copy(name = name)
|
||||
|
||||
@ -23,6 +27,8 @@ data class TestCordappImpl(override val name: String,
|
||||
|
||||
override fun withConfig(config: Map<String, Any>): TestCordappImpl = copy(config = config)
|
||||
|
||||
override fun signJar(keyStorePath: Path?): TestCordappImpl = copy(signJar = true, keyStorePath = keyStorePath)
|
||||
|
||||
fun withClasses(vararg classes: Class<*>): TestCordappImpl {
|
||||
return copy(classes = classes.filter { clazz -> packages.none { clazz.name.startsWith("$it.") } }.toSet())
|
||||
}
|
||||
|
@ -1,55 +0,0 @@
|
||||
package net.corda.testing.core
|
||||
|
||||
import net.corda.core.internal.JarSignatureCollector
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.nodeapi.internal.crypto.loadKeyStore
|
||||
import net.corda.testing.core.JarSignatureTestUtils.signJar
|
||||
import java.io.FileInputStream
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.security.PublicKey
|
||||
import java.util.jar.JarInputStream
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
object JarSignatureTestUtils {
|
||||
val bin = Paths.get(System.getProperty("java.home")).let { if (it.endsWith("jre")) it.parent else it } / "bin"
|
||||
|
||||
fun Path.executeProcess(vararg command: String) {
|
||||
val shredder = (this / "_shredder").toFile() // No need to delete after each test.
|
||||
assertEquals(0, ProcessBuilder()
|
||||
.inheritIO()
|
||||
.redirectOutput(shredder)
|
||||
.redirectError(shredder)
|
||||
.directory(this.toFile())
|
||||
.command((bin / command[0]).toString(), *command.sliceArray(1 until command.size))
|
||||
.start()
|
||||
.waitFor())
|
||||
}
|
||||
|
||||
fun Path.generateKey(alias: String, storePassword: String, name: String, keyalg: String = "RSA", keyPassword: String = storePassword, storeName: String = "_teststore") =
|
||||
executeProcess("keytool", "-genkeypair", "-keystore" ,storeName, "-storepass", storePassword, "-keyalg", keyalg, "-alias", alias, "-keypass", keyPassword, "-dname", name)
|
||||
|
||||
fun Path.createJar(fileName: String, vararg contents: String) =
|
||||
executeProcess(*(arrayOf("jar", "cvf", fileName) + contents))
|
||||
|
||||
fun Path.addIndexList(fileName: String) {
|
||||
executeProcess(*(arrayOf("jar", "i", fileName)))
|
||||
}
|
||||
|
||||
fun Path.updateJar(fileName: String, vararg contents: String) =
|
||||
executeProcess(*(arrayOf("jar", "uvf", fileName) + contents))
|
||||
|
||||
fun Path.signJar(fileName: String, alias: String, storePassword: String, keyPassword: String = storePassword): PublicKey {
|
||||
executeProcess("jarsigner", "-keystore", "_teststore", "-storepass", storePassword, "-keypass", keyPassword, fileName, alias)
|
||||
val ks = loadKeyStore(this.resolve("_teststore"), storePassword)
|
||||
return ks.getCertificate(alias).publicKey
|
||||
}
|
||||
|
||||
fun Path.getPublicKey(alias: String, storePassword: String) : PublicKey {
|
||||
val ks = loadKeyStore(this.resolve("_teststore"), storePassword)
|
||||
return ks.getCertificate(alias).publicKey
|
||||
}
|
||||
|
||||
fun Path.getJarSigners(fileName: String) =
|
||||
JarInputStream(FileInputStream((this / fileName).toFile())).use(JarSignatureCollector::collectSigners)
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
package net.corda.testing.core.internal
|
||||
|
||||
import net.corda.testing.core.internal.JarSignatureTestUtils.addManifest
|
||||
import net.corda.testing.core.internal.JarSignatureTestUtils.createJar
|
||||
import net.corda.testing.core.internal.JarSignatureTestUtils.generateKey
|
||||
import net.corda.testing.core.internal.JarSignatureTestUtils.signJar
|
||||
import net.corda.core.internal.delete
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.toPath
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import java.io.OutputStream
|
||||
import java.net.URI
|
||||
import java.net.URL
|
||||
import java.nio.file.*
|
||||
import java.security.PublicKey
|
||||
import java.util.jar.Attributes
|
||||
import java.util.jar.JarEntry
|
||||
import java.util.jar.JarOutputStream
|
||||
import javax.tools.JavaFileObject
|
||||
import javax.tools.SimpleJavaFileObject
|
||||
import javax.tools.StandardLocation
|
||||
import javax.tools.ToolProvider
|
||||
|
||||
object ContractJarTestUtils {
|
||||
|
||||
@JvmOverloads
|
||||
fun makeTestJar(output: OutputStream, extraEntries: List<Pair<String, String>> = emptyList()) {
|
||||
output.use {
|
||||
val jar = JarOutputStream(it)
|
||||
jar.putNextEntry(JarEntry("test1.txt"))
|
||||
jar.write("This is some useful content".toByteArray())
|
||||
jar.closeEntry()
|
||||
jar.putNextEntry(JarEntry("test2.txt"))
|
||||
jar.write("Some more useful content".toByteArray())
|
||||
extraEntries.forEach {
|
||||
jar.putNextEntry(JarEntry(it.first))
|
||||
jar.write(it.second.toByteArray())
|
||||
}
|
||||
jar.closeEntry()
|
||||
}
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun makeTestSignedContractJar(workingDir: Path, contractName: String, version: String = "1.0"): Pair<Path, PublicKey> {
|
||||
val alias = "testAlias"
|
||||
val pwd = "testPassword"
|
||||
workingDir.generateKey(alias, pwd, ALICE_NAME.toString())
|
||||
val jarName = makeTestContractJar(workingDir, contractName, true, version)
|
||||
val signer = workingDir.signJar(jarName.toAbsolutePath().toString(), alias, pwd)
|
||||
(workingDir / "_shredder").delete()
|
||||
(workingDir / "_teststore").delete()
|
||||
return workingDir.resolve(jarName) to signer
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun makeTestContractJar(workingDir: Path, contractName: String, signed: Boolean = false, version: String = "1.0"): Path {
|
||||
val packages = contractName.split(".")
|
||||
val jarName = "attachment-${packages.last()}-$version-${(if (signed) "signed" else "")}.jar"
|
||||
val className = packages.last()
|
||||
createTestClass(workingDir, className, packages.subList(0, packages.size - 1))
|
||||
workingDir.createJar(jarName, "${contractName.replace(".", "/")}.class")
|
||||
workingDir.addManifest(jarName, Pair(Attributes.Name.IMPLEMENTATION_VERSION, version))
|
||||
return workingDir.resolve(jarName)
|
||||
}
|
||||
|
||||
private fun createTestClass(workingDir: Path, className: String, packages: List<String>): Path {
|
||||
val newClass = """package ${packages.joinToString(".")};
|
||||
import net.corda.core.contracts.*;
|
||||
import net.corda.core.transactions.*;
|
||||
|
||||
public class $className implements Contract {
|
||||
@Override
|
||||
public void verify(LedgerTransaction tx) throws IllegalArgumentException {
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val compiler = ToolProvider.getSystemJavaCompiler()
|
||||
val source = object : SimpleJavaFileObject(URI.create("string:///${packages.joinToString("/")}/$className.java"), JavaFileObject.Kind.SOURCE) {
|
||||
override fun getCharContent(ignoreEncodingErrors: Boolean): CharSequence {
|
||||
return newClass
|
||||
}
|
||||
}
|
||||
val fileManager = compiler.getStandardFileManager(null, null, null)
|
||||
fileManager.setLocation(StandardLocation.CLASS_OUTPUT, listOf(workingDir.toFile()))
|
||||
|
||||
compiler.getTask(System.out.writer(), fileManager, null, null, null, listOf(source)).call()
|
||||
val outFile = fileManager.getFileForInput(StandardLocation.CLASS_OUTPUT, packages.joinToString("."), "$className.class")
|
||||
return Paths.get(outFile.name)
|
||||
}
|
||||
|
||||
fun signContractJar(jarURL: URL, copyFirst: Boolean, keyStoreDir: Path? = null, alias: String = "testAlias", pwd: String = "testPassword"): Pair<Path, PublicKey> {
|
||||
val jarName =
|
||||
if (copyFirst) {
|
||||
val signedJarName = Paths.get(jarURL.path.substringBeforeLast(".") + "-SIGNED.jar")
|
||||
Files.copy(jarURL.toPath(), signedJarName, StandardCopyOption.REPLACE_EXISTING)
|
||||
signedJarName
|
||||
}
|
||||
else jarURL.toPath()
|
||||
|
||||
val workingDir =
|
||||
if (keyStoreDir == null) {
|
||||
val workingDir = jarName.parent
|
||||
workingDir.generateKey(alias, pwd, ALICE_NAME.toString())
|
||||
workingDir
|
||||
} else keyStoreDir
|
||||
|
||||
val signer = workingDir.signJar(jarName.toAbsolutePath().toString(), alias, pwd)
|
||||
(workingDir / "_shredder").delete()
|
||||
(workingDir / "_teststore").delete()
|
||||
return workingDir.resolve(jarName) to signer
|
||||
}
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
package net.corda.testing.core.internal
|
||||
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.JarSignatureCollector
|
||||
import net.corda.core.internal.deleteRecursively
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.nodeapi.internal.crypto.loadKeyStore
|
||||
import java.io.Closeable
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.security.PublicKey
|
||||
import java.util.jar.Attributes
|
||||
import java.util.jar.JarInputStream
|
||||
import java.util.jar.JarOutputStream
|
||||
import java.util.jar.Manifest
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
/**
|
||||
* Class to create an automatically delete a temporary directory.
|
||||
*/
|
||||
class SelfCleaningDir : Closeable {
|
||||
val path: Path = Files.createTempDirectory(JarSignatureTestUtils::class.simpleName)
|
||||
override fun close() {
|
||||
path.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
object JarSignatureTestUtils {
|
||||
val bin = Paths.get(System.getProperty("java.home")).let { if (it.endsWith("jre")) it.parent else it } / "bin"
|
||||
|
||||
fun Path.executeProcess(vararg command: String) {
|
||||
val shredder = (this / "_shredder").toFile() // No need to delete after each test.
|
||||
assertEquals(0, ProcessBuilder()
|
||||
.inheritIO()
|
||||
.redirectOutput(shredder)
|
||||
.redirectError(shredder)
|
||||
.directory(this.toFile())
|
||||
.command((bin / command[0]).toString(), *command.sliceArray(1 until command.size))
|
||||
.start()
|
||||
.waitFor())
|
||||
}
|
||||
|
||||
val CODE_SIGNER = CordaX500Name("Test Code Signing Service", "London", "GB")
|
||||
|
||||
fun Path.generateKey(alias: String = "Test", storePassword: String = "secret!", name: String = CODE_SIGNER.toString(), keyalg: String = "RSA", keyPassword: String = storePassword, storeName: String = "_teststore") : PublicKey {
|
||||
executeProcess("keytool", "-genkeypair", "-keystore", storeName, "-storepass", storePassword, "-keyalg", keyalg, "-alias", alias, "-keypass", keyPassword, "-dname", name)
|
||||
val ks = loadKeyStore(this.resolve("_teststore"), storePassword)
|
||||
return ks.getCertificate(alias).publicKey
|
||||
}
|
||||
|
||||
fun Path.createJar(fileName: String, vararg contents: String) =
|
||||
executeProcess(*(arrayOf("jar", "cvf", fileName) + contents))
|
||||
|
||||
fun Path.addIndexList(fileName: String) {
|
||||
executeProcess(*(arrayOf("jar", "i", fileName)))
|
||||
}
|
||||
|
||||
fun Path.updateJar(fileName: String, vararg contents: String) =
|
||||
executeProcess(*(arrayOf("jar", "uvf", fileName) + contents))
|
||||
|
||||
fun Path.signJar(fileName: String, alias: String, storePassword: String, keyPassword: String = storePassword): PublicKey {
|
||||
executeProcess("jarsigner", "-keystore", "_teststore", "-storepass", storePassword, "-keypass", keyPassword, fileName, alias)
|
||||
val ks = loadKeyStore(this.resolve("_teststore"), storePassword)
|
||||
return ks.getCertificate(alias).publicKey
|
||||
}
|
||||
|
||||
fun Path.getPublicKey(alias: String, storePassword: String) : PublicKey {
|
||||
val ks = loadKeyStore(this.resolve("_teststore"), storePassword)
|
||||
return ks.getCertificate(alias).publicKey
|
||||
}
|
||||
|
||||
fun Path.getJarSigners(fileName: String) =
|
||||
JarInputStream(FileInputStream((this / fileName).toFile())).use(JarSignatureCollector::collectSigners)
|
||||
|
||||
fun Path.addManifest(fileName: String, vararg entry: Pair<Attributes.Name, String>) {
|
||||
JarInputStream(FileInputStream((this / fileName).toFile())).use { input ->
|
||||
val manifest = input.manifest ?: Manifest()
|
||||
entry.forEach { (attributeName, value) ->
|
||||
// eg. Attributes.Name.IMPLEMENTATION_VERSION, version
|
||||
manifest.mainAttributes[attributeName] = value
|
||||
}
|
||||
val output = JarOutputStream(FileOutputStream((this / fileName).toFile()), manifest)
|
||||
var entry= input.nextEntry
|
||||
val buffer = ByteArray(1 shl 14)
|
||||
while (true) {
|
||||
output.putNextEntry(entry)
|
||||
var nr: Int
|
||||
while (true) {
|
||||
nr = input.read(buffer)
|
||||
if (nr < 0) break
|
||||
output.write(buffer, 0, nr)
|
||||
}
|
||||
entry = input.nextEntry ?: break
|
||||
}
|
||||
output.close()
|
||||
}
|
||||
}
|
||||
}
|
@ -5,15 +5,15 @@ import net.corda.core.contracts.ContractAttachment
|
||||
import net.corda.core.contracts.ContractClassName
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.AbstractAttachment
|
||||
import net.corda.core.internal.JarSignatureCollector
|
||||
import net.corda.core.internal.UNKNOWN_UPLOADER
|
||||
import net.corda.core.internal.readFully
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
import net.corda.core.node.services.AttachmentStorage
|
||||
import net.corda.core.node.services.vault.AttachmentQueryCriteria
|
||||
import net.corda.core.node.services.vault.AttachmentSort
|
||||
import net.corda.core.node.services.vault.Builder
|
||||
import net.corda.core.node.services.vault.ColumnPredicate
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.nodeapi.internal.withContractsInJar
|
||||
import java.io.InputStream
|
||||
@ -26,7 +26,10 @@ import java.util.jar.JarInputStream
|
||||
*/
|
||||
class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() {
|
||||
|
||||
private data class ContractAttachmentMetadata(val name: ContractClassName, val version: String, val isSigned: Boolean)
|
||||
|
||||
private val _files = HashMap<SecureHash, Pair<Attachment, ByteArray>>()
|
||||
private val _contractClasses = HashMap<ContractAttachmentMetadata, SecureHash>()
|
||||
/** A map of the currently stored files by their [SecureHash] */
|
||||
val files: Map<SecureHash, Pair<Attachment, ByteArray>> get() = _files
|
||||
|
||||
@ -41,8 +44,27 @@ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() {
|
||||
|
||||
override fun openAttachment(id: SecureHash): Attachment? = files[id]?.first
|
||||
|
||||
override fun queryAttachments(criteria: AttachmentQueryCriteria, sorting: AttachmentSort?): List<AttachmentId> {
|
||||
throw NotImplementedError("Querying for attachments not implemented")
|
||||
override fun queryAttachments(criteria: AttachmentQueryCriteria, sorting: AttachmentSort?): List<SecureHash> {
|
||||
criteria as AttachmentQueryCriteria.AttachmentsQueryCriteria
|
||||
val contractClassNames =
|
||||
if (criteria.contractClassNamesCondition is ColumnPredicate.EqualityComparison)
|
||||
(criteria.contractClassNamesCondition as ColumnPredicate.EqualityComparison<List<ContractClassName>>).rightLiteral
|
||||
else emptyList()
|
||||
val contractMetadataList =
|
||||
if (criteria.isSignedCondition != null) {
|
||||
val isSigned = criteria.isSignedCondition == Builder.equal(true)
|
||||
contractClassNames.map {contractClassName ->
|
||||
ContractAttachmentMetadata(contractClassName, "1.0", isSigned)
|
||||
}
|
||||
}
|
||||
else {
|
||||
contractClassNames.flatMap { contractClassName ->
|
||||
listOf(ContractAttachmentMetadata(contractClassName, "1.0", false),
|
||||
ContractAttachmentMetadata(contractClassName, "1.0", true))
|
||||
}
|
||||
}
|
||||
|
||||
return _contractClasses.filterKeys { contractMetadataList.contains(it) }.values.toList()
|
||||
}
|
||||
|
||||
override fun hasAttachment(attachmentId: AttachmentId) = files.containsKey(attachmentId)
|
||||
@ -59,6 +81,10 @@ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() {
|
||||
@JvmOverloads
|
||||
fun importContractAttachment(contractClassNames: List<ContractClassName>, uploader: String, jar: InputStream, attachmentId: AttachmentId? = null, signers: List<PublicKey> = emptyList()): AttachmentId = importAttachmentInternal(jar, uploader, contractClassNames, attachmentId, signers)
|
||||
|
||||
fun importContractAttachment(attachmentId: AttachmentId, contractAttachment: ContractAttachment) {
|
||||
_files[attachmentId] = Pair(contractAttachment, ByteArray(1))
|
||||
}
|
||||
|
||||
fun getAttachmentIdAndBytes(jar: InputStream): Pair<AttachmentId, ByteArray> = jar.readFully().let { bytes -> Pair(bytes.sha256(), bytes) }
|
||||
|
||||
private class MockAttachment(dataLoader: () -> ByteArray, override val id: SecureHash, override val signerKeys: List<PublicKey>) : AbstractAttachment(dataLoader)
|
||||
@ -72,7 +98,15 @@ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() {
|
||||
val sha256 = attachmentId ?: bytes.sha256()
|
||||
if (sha256 !in files.keys) {
|
||||
val baseAttachment = MockAttachment({ bytes }, sha256, signers)
|
||||
val attachment = if (contractClassNames == null || contractClassNames.isEmpty()) baseAttachment else ContractAttachment(baseAttachment, contractClassNames.first(), contractClassNames.toSet(), uploader, signers)
|
||||
val attachment =
|
||||
if (contractClassNames == null || contractClassNames.isEmpty()) baseAttachment
|
||||
else {
|
||||
contractClassNames.map {contractClassName ->
|
||||
val contractClassMetadata = ContractAttachmentMetadata(contractClassName, "1.0", signers.isNotEmpty())
|
||||
_contractClasses[contractClassMetadata] = sha256
|
||||
}
|
||||
ContractAttachment(baseAttachment, contractClassNames.first(), contractClassNames.toSet(), uploader, signers, "1.0")
|
||||
}
|
||||
_files[sha256] = Pair(attachment, bytes)
|
||||
}
|
||||
return sha256
|
||||
|
@ -1,16 +1,13 @@
|
||||
package net.corda.testing.core
|
||||
|
||||
import net.corda.testing.core.JarSignatureTestUtils.createJar
|
||||
import net.corda.testing.core.JarSignatureTestUtils.generateKey
|
||||
import net.corda.testing.core.JarSignatureTestUtils.getJarSigners
|
||||
import net.corda.testing.core.JarSignatureTestUtils.signJar
|
||||
import net.corda.testing.core.JarSignatureTestUtils.updateJar
|
||||
import net.corda.testing.core.JarSignatureTestUtils.addIndexList
|
||||
import net.corda.testing.core.internal.JarSignatureTestUtils.createJar
|
||||
import net.corda.testing.core.internal.JarSignatureTestUtils.generateKey
|
||||
import net.corda.testing.core.internal.JarSignatureTestUtils.getJarSigners
|
||||
import net.corda.testing.core.internal.JarSignatureTestUtils.signJar
|
||||
import net.corda.testing.core.internal.JarSignatureTestUtils.updateJar
|
||||
import net.corda.testing.core.internal.JarSignatureTestUtils.addIndexList
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.CHARLIE_NAME
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.After
|
||||
import org.junit.AfterClass
|
||||
|
@ -11,8 +11,8 @@ import net.corda.nodeapi.internal.network.NetworkBootstrapperWithOverridablePara
|
||||
import net.corda.nodeapi.internal.network.NetworkParametersOverrides
|
||||
import net.corda.nodeapi.internal.network.PackageOwner
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.JarSignatureTestUtils.generateKey
|
||||
import net.corda.testing.core.JarSignatureTestUtils.getPublicKey
|
||||
import net.corda.testing.core.internal.JarSignatureTestUtils.generateKey
|
||||
import net.corda.testing.core.internal.JarSignatureTestUtils.getPublicKey
|
||||
import org.junit.*
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.FileNotFoundException
|
||||
|
Loading…
Reference in New Issue
Block a user