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:
josecoll 2018-12-04 18:45:29 +00:00 committed by GitHub
parent 382e3b651f
commit 63e326aedb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 1349 additions and 341 deletions

View File

@ -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)

View File

@ -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
}
}

View File

@ -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')"
}
}

View File

@ -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)

View File

@ -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"
}
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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>

View File

@ -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)
}
/**

View File

@ -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) }) {

View File

@ -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)
}
}

View File

@ -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)
}
/**

View File

@ -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?,

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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) .

View File

@ -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

View File

@ -319,6 +319,8 @@ An example configuration file:
}
]
.. _package_namespace_ownership:
Package namespace ownership
~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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 {

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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();
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}

View File

@ -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 {
/**

View File

@ -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

View File

@ -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())
}

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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()
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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