mirror of
https://github.com/corda/corda.git
synced 2025-04-19 08:36:39 +00:00
Merge pull request #1499 from corda/tudor_os_merge_23_10
Tudor os merge 23 10
This commit is contained in:
commit
9edc15d018
@ -429,7 +429,7 @@ public static final class net.corda.core.contracts.AmountTransfer$Companion exte
|
||||
public interface net.corda.core.contracts.Attachment extends net.corda.core.contracts.NamedByHash
|
||||
public void extractFile(String, java.io.OutputStream)
|
||||
@NotNull
|
||||
public abstract java.util.List<net.corda.core.identity.Party> getSigners()
|
||||
public abstract java.util.List<java.security.PublicKey> getSigners()
|
||||
public abstract int getSize()
|
||||
@NotNull
|
||||
public abstract java.io.InputStream open()
|
||||
@ -540,7 +540,7 @@ public final class net.corda.core.contracts.ContractAttachment extends java.lang
|
||||
@NotNull
|
||||
public net.corda.core.crypto.SecureHash getId()
|
||||
@NotNull
|
||||
public java.util.List<net.corda.core.identity.Party> getSigners()
|
||||
public java.util.List<java.security.PublicKey> getSigners()
|
||||
public int getSize()
|
||||
@Nullable
|
||||
public final String getUploader()
|
||||
|
@ -47,13 +47,13 @@ buildscript {
|
||||
ext.hamkrest_version = '1.4.2.2'
|
||||
ext.jopt_simple_version = '5.0.2'
|
||||
ext.jansi_version = '1.14'
|
||||
ext.hibernate_version = '5.2.6.Final'
|
||||
ext.hibernate_version = '5.3.6.Final'
|
||||
ext.h2_version = '1.4.197' // Update docs if renamed or removed.
|
||||
ext.postgresql_version = '42.1.4'
|
||||
ext.rxjava_version = '1.3.8'
|
||||
ext.dokka_version = '0.9.17'
|
||||
ext.eddsa_version = '0.3.0' // Performance tuned version for enterprise.
|
||||
ext.dependency_checker_version = '3.1.0'
|
||||
ext.dependency_checker_version = '3.3.2'
|
||||
ext.commons_collections_version = '4.1'
|
||||
ext.beanutils_version = '1.9.3'
|
||||
ext.crash_version = 'cadb53544fbb3c0fb901445da614998a6a419488'
|
||||
|
@ -229,6 +229,15 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
|
||||
fun `SignedTransaction (WireTransaction)`() {
|
||||
val attachmentId = SecureHash.randomSHA256()
|
||||
doReturn(attachmentId).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID)
|
||||
val attachmentStorage = rigorousMock<AttachmentStorage>()
|
||||
doReturn(attachmentStorage).whenever(services).attachments
|
||||
val attachment = rigorousMock<ContractAttachment>()
|
||||
doReturn(attachment).whenever(attachmentStorage).openAttachment(attachmentId)
|
||||
doReturn(attachmentId).whenever(attachment).id
|
||||
doReturn(emptyList<Party>()).whenever(attachment).signers
|
||||
doReturn(setOf(DummyContract.PROGRAM_ID)).whenever(attachment).allContracts
|
||||
doReturn("app").whenever(attachment).uploader
|
||||
|
||||
val wtx = TransactionBuilder(
|
||||
notary = DUMMY_NOTARY,
|
||||
inputs = mutableListOf(StateRef(SecureHash.randomSHA256(), 1)),
|
||||
|
@ -191,10 +191,10 @@ class NodeMonitorModel : AutoCloseable {
|
||||
val retryInterval = 5.seconds
|
||||
|
||||
val client = CordaRPCClient(
|
||||
nodeHostAndPort,
|
||||
CordaRPCClientConfiguration.DEFAULT.copy(
|
||||
connectionMaxRetryInterval = retryInterval
|
||||
)
|
||||
nodeHostAndPort,
|
||||
CordaRPCClientConfiguration.DEFAULT.copy(
|
||||
connectionMaxRetryInterval = retryInterval
|
||||
)
|
||||
)
|
||||
do {
|
||||
val connection = try {
|
||||
|
@ -1,5 +1,5 @@
|
||||
gradlePluginsVersion=4.0.32
|
||||
kotlinVersion=1.2.51
|
||||
kotlinVersion=1.2.71
|
||||
# ***************************************************************#
|
||||
# When incrementing platformVersion make sure to update #
|
||||
# net.corda.core.internal.CordaUtilsKt.PLATFORM_VERSION as well. #
|
||||
|
@ -42,8 +42,8 @@ class AttachmentTest {
|
||||
attachment = object : Attachment {
|
||||
override val id: SecureHash
|
||||
get() = SecureHash.allOnesHash
|
||||
override val signers: List<Party>
|
||||
get() = listOf(ALICE)
|
||||
override val signers: List<PublicKey>
|
||||
get() = listOf(ALICE_KEY)
|
||||
override val size: Int
|
||||
get() = jarData.size
|
||||
|
||||
|
@ -3,13 +3,13 @@ package net.corda.deterministic.verifier
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.contracts.ContractClassName
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
import java.security.PublicKey
|
||||
|
||||
@CordaSerializable
|
||||
class MockContractAttachment(override val id: SecureHash = SecureHash.zeroHash, val contract: ContractClassName, override val signers: List<Party> = ArrayList()) : Attachment {
|
||||
class MockContractAttachment(override val id: SecureHash = SecureHash.zeroHash, val contract: ContractClassName, override val signers: List<PublicKey> = emptyList()) : Attachment {
|
||||
override fun open(): InputStream = ByteArrayInputStream(id.bytes)
|
||||
override val size = id.size
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import net.corda.core.serialization.CordaSerializable
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.security.PublicKey
|
||||
import java.util.jar.JarInputStream
|
||||
|
||||
/**
|
||||
@ -51,10 +52,10 @@ interface Attachment : NamedByHash {
|
||||
fun extractFile(path: String, outputTo: OutputStream) = openAsJAR().use { it.extractFile(path, outputTo) }
|
||||
|
||||
/**
|
||||
* The parties that have correctly signed the whole attachment.
|
||||
* The keys that have correctly signed the whole attachment.
|
||||
* Can be empty, for example non-contract attachments won't be necessarily be signed.
|
||||
*/
|
||||
val signers: List<Party>
|
||||
val signers: List<PublicKey>
|
||||
|
||||
/**
|
||||
* Attachment size in bytes.
|
||||
|
@ -3,20 +3,80 @@ package net.corda.core.contracts
|
||||
import net.corda.core.DoNotImplement
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint.isSatisfiedBy
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.SecureHash
|
||||
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.CordaSerializable
|
||||
import net.corda.core.utilities.warnOnce
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.lang.annotation.Inherited
|
||||
import java.security.PublicKey
|
||||
|
||||
/** Constrain which contract-code-containing attachment can be used with a [ContractState]. */
|
||||
/**
|
||||
* This annotation should only be added to [Contract] classes.
|
||||
* If the annotation is present, then we assume that [Contract.verify] will ensure that the output states have an acceptable constraint.
|
||||
* If the annotation is missing, then the default - secure - constraint propagation logic is enforced by the platform.
|
||||
*/
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Inherited
|
||||
annotation class NoConstraintPropagation
|
||||
|
||||
/**
|
||||
* Constrain which contract-code-containing attachment can be used with a [Contract].
|
||||
* */
|
||||
@CordaSerializable
|
||||
@DoNotImplement
|
||||
interface AttachmentConstraint {
|
||||
/** Returns whether the given contract attachment can be used with the [ContractState] associated with this constraint object. */
|
||||
fun isSatisfiedBy(attachment: Attachment): Boolean
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* TODO - SignatureConstraint third party signers.
|
||||
*/
|
||||
fun canBeTransitionedFrom(input: AttachmentConstraint, attachment: ContractAttachment): Boolean {
|
||||
val output = this
|
||||
return when {
|
||||
// These branches should not happen, as this has been already checked.
|
||||
input is AutomaticPlaceholderConstraint || output is AutomaticPlaceholderConstraint -> throw IllegalArgumentException("Illegal constraint: AutomaticPlaceholderConstraint.")
|
||||
input is AutomaticHashConstraint || output is AutomaticHashConstraint -> throw IllegalArgumentException("Illegal constraint: AutomaticHashConstraint.")
|
||||
|
||||
// Transition to the same constraint.
|
||||
input == output -> true
|
||||
|
||||
// You can't transition from the AlwaysAcceptAttachmentConstraint to anything else, as it could hide something illegal.
|
||||
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
|
||||
|
||||
// The SignatureAttachmentConstraint allows migration from a Signature constraint with the same key.
|
||||
// TODO - we don't support currently third party signers. When we do, the output key will have to be stronger then the input key.
|
||||
input is SignatureAttachmentConstraint && output is SignatureAttachmentConstraint -> input.key == output.key
|
||||
|
||||
// You can transition from the WhitelistConstraint to the SignatureConstraint only if all signers of the JAR are required to sign in the future.
|
||||
input is WhitelistedByZoneAttachmentConstraint && output is SignatureAttachmentConstraint ->
|
||||
attachment.signers.isNotEmpty() && output.key.keys.containsAll(attachment.signers)
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** An [AttachmentConstraint] where [isSatisfiedBy] always returns true. */
|
||||
@ -48,26 +108,64 @@ 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")
|
||||
val whitelist = attachment.whitelistedContractImplementations
|
||||
?: throw IllegalStateException("Unable to verify WhitelistedByZoneAttachmentConstraint - whitelist not specified")
|
||||
attachment.id in (whitelist[attachment.stateContract] ?: emptyList())
|
||||
} else false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This [AttachmentConstraint] is a convenience class that will be automatically resolved to a [HashAttachmentConstraint].
|
||||
* The resolution occurs in [TransactionBuilder.toWireTransaction] and uses the [TransactionState.contract] value
|
||||
* to find a corresponding loaded [Cordapp] that contains such a contract, and then uses that [Cordapp] as the
|
||||
* [Attachment].
|
||||
*
|
||||
* If, for any reason, this class is not automatically resolved the default implementation is to fail, because the
|
||||
* intent of this class is that it should be replaced by a correct [HashAttachmentConstraint] and verify against an
|
||||
* actual [Attachment].
|
||||
*/
|
||||
@KeepForDJVM
|
||||
@Deprecated("The name is no longer valid as multiple constraints were added.", replaceWith = ReplaceWith("AutomaticPlaceholderConstraint"), level = DeprecationLevel.WARNING)
|
||||
object AutomaticHashConstraint : AttachmentConstraint {
|
||||
override fun isSatisfiedBy(attachment: Attachment): Boolean {
|
||||
throw UnsupportedOperationException("Contracts cannot be satisfied by an AutomaticHashConstraint placeholder")
|
||||
throw UnsupportedOperationException("Contracts cannot be satisfied by an AutomaticHashConstraint placeholder.")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This [AttachmentConstraint] is a convenience class that acts as a placeholder and will be automatically resolved by the platform when set on an output state.
|
||||
* It is the default constraint of all output states.
|
||||
*
|
||||
* The resolution occurs in [TransactionBuilder.toWireTransaction] and is based on the input states and the attachments.
|
||||
* If the [Contract] was not annotated with [NoConstraintPropagation], then the platform will ensure the correct constraint propagation.
|
||||
*/
|
||||
@KeepForDJVM
|
||||
object AutomaticPlaceholderConstraint : AttachmentConstraint {
|
||||
override fun isSatisfiedBy(attachment: Attachment): Boolean {
|
||||
throw UnsupportedOperationException("Contracts cannot be satisfied by an AutomaticPlaceholderConstraint placeholder.")
|
||||
}
|
||||
}
|
||||
|
||||
private val logger = LoggerFactory.getLogger(AttachmentConstraint::class.java)
|
||||
private val validConstraints = setOf(
|
||||
AlwaysAcceptAttachmentConstraint::class,
|
||||
HashAttachmentConstraint::class,
|
||||
WhitelistedByZoneAttachmentConstraint::class,
|
||||
SignatureAttachmentConstraint::class)
|
||||
|
||||
/**
|
||||
* Fails if the constraint is not of a known type.
|
||||
* Only the Corda core is allowed to implement the [AttachmentConstraint] interface.
|
||||
*/
|
||||
internal fun checkConstraintValidity(state: TransactionState<*>) {
|
||||
require(state.constraint::class in validConstraints) { "Found state ${state.contract} with an illegal constraint: ${state.constraint}" }
|
||||
if (state.constraint is AlwaysAcceptAttachmentConstraint) {
|
||||
logger.warnOnce("Found state ${state.contract} that is constrained by the insecure: AlwaysAcceptAttachmentConstraint.")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for the [NoConstraintPropagation] annotation on the contractClassName.
|
||||
* If it's present it means that the automatic secure core behaviour is not applied, and it's up to the contract developer to enforce a secure propagation logic.
|
||||
*/
|
||||
internal fun ContractClassName.contractHasAutomaticConstraintPropagation(classLoader: ClassLoader? = null) =
|
||||
(classLoader ?: NoConstraintPropagation::class.java.classLoader)
|
||||
.loadClass(this).getAnnotation(NoConstraintPropagation::class.java) == null
|
||||
|
||||
fun ContractClassName.warnContractWithoutConstraintPropagation(classLoader: ClassLoader? = null) {
|
||||
if (!this.contractHasAutomaticConstraintPropagation(classLoader)) {
|
||||
logger.warnOnce("Found contract $this with automatic constraint propagation disabled.")
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,5 +180,5 @@ data class SignatureAttachmentConstraint(
|
||||
val key: PublicKey
|
||||
) : AttachmentConstraint {
|
||||
override fun isSatisfiedBy(attachment: Attachment): Boolean =
|
||||
key.isFulfilledBy(attachment.signers.map { it.owningKey })
|
||||
key.isFulfilledBy(attachment.signers.map { it })
|
||||
}
|
@ -2,6 +2,7 @@ package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.security.PublicKey
|
||||
|
||||
/**
|
||||
* Wrap an attachment in this if it is to be used as an executable contract attachment
|
||||
@ -12,7 +13,12 @@ import net.corda.core.serialization.CordaSerializable
|
||||
*/
|
||||
@KeepForDJVM
|
||||
@CordaSerializable
|
||||
class ContractAttachment @JvmOverloads constructor(val attachment: Attachment, val contract: ContractClassName, val additionalContracts: Set<ContractClassName> = emptySet(), val uploader: String? = null) : Attachment by attachment {
|
||||
class ContractAttachment @JvmOverloads constructor(
|
||||
val attachment: Attachment,
|
||||
val contract: ContractClassName,
|
||||
val additionalContracts: Set<ContractClassName> = emptySet(),
|
||||
val uploader: String? = null,
|
||||
override val signers: List<PublicKey> = emptyList()) : Attachment by attachment {
|
||||
|
||||
val allContracts: Set<ContractClassName> get() = additionalContracts + contract
|
||||
|
||||
|
@ -28,8 +28,8 @@ data class TransactionState<out T : ContractState> @JvmOverloads constructor(
|
||||
*/
|
||||
// TODO: Implement the contract sandbox loading of the contract attachments
|
||||
val contract: ContractClassName = requireNotNull(data.requiredContractClassName) {
|
||||
//TODO: add link to docsite page, when there is one.
|
||||
"""
|
||||
//TODO: add link to docsite page, when there is one.
|
||||
"""
|
||||
Unable to infer Contract class name because state class ${data::class.java.name} is not annotated with
|
||||
@BelongsToContract, and does not have an enclosing class which implements Contract. Either annotate ${data::class.java.name}
|
||||
with @BelongsToContract, or supply an explicit contract parameter to TransactionState().
|
||||
@ -58,7 +58,7 @@ data class TransactionState<out T : ContractState> @JvmOverloads constructor(
|
||||
/**
|
||||
* A validator for the contract attachments on the transaction.
|
||||
*/
|
||||
val constraint: AttachmentConstraint = AutomaticHashConstraint) {
|
||||
val constraint: AttachmentConstraint = AutomaticPlaceholderConstraint) {
|
||||
|
||||
private companion object {
|
||||
val logger = loggerFor<TransactionState<*>>()
|
||||
|
@ -5,6 +5,7 @@ import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.utilities.NonEmptySet
|
||||
import java.security.PublicKey
|
||||
@ -36,7 +37,7 @@ class AttachmentResolutionException(val hash: SecureHash) : FlowException("Attac
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
@CordaSerializable
|
||||
sealed class TransactionVerificationException(val txId: SecureHash, message: String, cause: Throwable?)
|
||||
abstract class TransactionVerificationException(val txId: SecureHash, message: String, cause: Throwable?)
|
||||
: FlowException("$message, transaction: $txId", cause) {
|
||||
|
||||
/**
|
||||
@ -50,6 +51,19 @@ sealed class TransactionVerificationException(val txId: SecureHash, message: Str
|
||||
constructor(txId: SecureHash, contract: Contract, cause: Throwable) : this(txId, contract.javaClass.name, cause)
|
||||
}
|
||||
|
||||
/**
|
||||
* This exception happens when a transaction was not built correctly.
|
||||
* When a contract is not annotated with [NoConstraintPropagation], then the platform ensures that the constraints of output states transition correctly from input states.
|
||||
*
|
||||
* @property txId The transaction.
|
||||
* @property contractClass The fully qualified class name of the failing contract.
|
||||
* @property inputConstraint The constraint of the input state.
|
||||
* @property outputConstraint The constraint of the outputs state.
|
||||
*/
|
||||
@KeepForDJVM
|
||||
class ConstraintPropagationRejection(txId: SecureHash, val contractClass: String, inputConstraint: AttachmentConstraint, outputConstraint: AttachmentConstraint)
|
||||
: TransactionVerificationException(txId, "Contract constraints for $contractClass are not propagated correctly. The outputConstraint: $outputConstraint is not a valid transition from the input constraint: $inputConstraint.", null)
|
||||
|
||||
/**
|
||||
* The transaction attachment that contains the [contractClass] class didn't meet the constraints specified by
|
||||
* the [TransactionState.constraint] object. This usually implies a version mismatch of some kind.
|
||||
@ -169,4 +183,12 @@ sealed class TransactionVerificationException(val txId: SecureHash, message: Str
|
||||
@DeleteForDJVM
|
||||
class InvalidNotaryChange(txId: SecureHash)
|
||||
: TransactionVerificationException(txId, "Detected a notary change. Outputs must use the same notary as inputs", null)
|
||||
|
||||
/**
|
||||
* Thrown to indicate that a contract attachment is not signed by the network-wide package owner.
|
||||
*/
|
||||
class ContractAttachmentNotSignedByPackageOwnerException(txId: SecureHash, val attachmentHash: AttachmentId, val contractClass: String) : TransactionVerificationException(txId,
|
||||
"""The Contract attachment JAR: $attachmentHash containing the contract: $contractClass is not signed by the owner specified in the network parameters.
|
||||
Please check the source of this attachment and if it is malicious contact your zone operator to report this incident.
|
||||
For details see: https://docs.corda.net/network-map.html#network-parameters""".trimIndent(), null)
|
||||
}
|
||||
|
@ -119,7 +119,7 @@ open class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any)
|
||||
/**
|
||||
* This is a wildcard payload to be used by the invoker of the [DataVendingFlow] to allow unlimited access to its vault.
|
||||
*
|
||||
* Todo Fails with a serialization exception if it is not a list. Why?
|
||||
* TODO Fails with a serialization exception if it is not a list. Why?
|
||||
*/
|
||||
@CordaSerializable
|
||||
object RetrieveAnyTransactionPayload : ArrayList<Any>()
|
@ -1,4 +1,5 @@
|
||||
@file:KeepForDJVM
|
||||
|
||||
package net.corda.core.internal
|
||||
|
||||
import net.corda.core.DeleteForDJVM
|
||||
@ -11,6 +12,7 @@ import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.security.PublicKey
|
||||
import java.util.jar.JarInputStream
|
||||
|
||||
const val DEPLOYED_CORDAPP_UPLOADER = "app"
|
||||
@ -40,8 +42,8 @@ abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment {
|
||||
override val size: Int get() = attachmentData.size
|
||||
|
||||
override fun open(): InputStream = attachmentData.inputStream()
|
||||
override val signers by lazy {
|
||||
openAsJAR().use(JarSignatureCollector::collectSigningParties)
|
||||
override val signers: List<PublicKey> by lazy {
|
||||
openAsJAR().use(JarSignatureCollector::collectSigners)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?) = other === this || other is Attachment && other.id == this.id
|
||||
|
@ -2,6 +2,7 @@ package net.corda.core.internal
|
||||
|
||||
import net.corda.core.identity.Party
|
||||
import java.security.CodeSigner
|
||||
import java.security.PublicKey
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.jar.JarEntry
|
||||
import java.util.jar.JarInputStream
|
||||
@ -23,22 +24,25 @@ object JarSignatureCollector {
|
||||
* @param jar The open [JarInputStream] to collect signing parties from.
|
||||
* @throws InvalidJarSignersException If the signer sets for any two signable items are different from each other.
|
||||
*/
|
||||
fun collectSigningParties(jar: JarInputStream): List<Party> {
|
||||
fun collectSigners(jar: JarInputStream): List<PublicKey> = getSigners(jar).toOrderedPublicKeys()
|
||||
|
||||
fun collectSigningParties(jar: JarInputStream): List<Party> = getSigners(jar).toPartiesOrderedByName()
|
||||
|
||||
private fun getSigners(jar: JarInputStream): Set<CodeSigner> {
|
||||
val signerSets = jar.fileSignerSets
|
||||
if (signerSets.isEmpty()) return emptyList()
|
||||
if (signerSets.isEmpty()) return emptySet()
|
||||
|
||||
val (firstFile, firstSignerSet) = signerSets.first()
|
||||
for ((otherFile, otherSignerSet) in signerSets.subList(1, signerSets.size)) {
|
||||
if (otherSignerSet != firstSignerSet) throw InvalidJarSignersException(
|
||||
"""
|
||||
Mismatch between signers ${firstSignerSet.toPartiesOrderedByName()} for file $firstFile
|
||||
and signers ${otherSignerSet.toPartiesOrderedByName()} for file ${otherFile}.
|
||||
See https://docs.corda.net/design/data-model-upgrades/signature-constraints.html for details of the
|
||||
constraints applied to attachment signatures.
|
||||
""".trimIndent().replace('\n', ' '))
|
||||
"""
|
||||
Mismatch between signers ${firstSignerSet.toOrderedPublicKeys()} for file $firstFile
|
||||
and signers ${otherSignerSet.toOrderedPublicKeys()} for file ${otherFile}.
|
||||
See https://docs.corda.net/design/data-model-upgrades/signature-constraints.html for details of the
|
||||
constraints applied to attachment signatures.
|
||||
""".trimIndent().replace('\n', ' '))
|
||||
}
|
||||
|
||||
return firstSignerSet.toPartiesOrderedByName()
|
||||
return firstSignerSet
|
||||
}
|
||||
|
||||
private val JarInputStream.fileSignerSets: List<Pair<String, Set<CodeSigner>>> get() =
|
||||
@ -59,6 +63,10 @@ object JarSignatureCollector {
|
||||
private fun Sequence<JarEntry>.toFileSignerSet(): Sequence<Pair<String, Set<CodeSigner>>> =
|
||||
map { entry -> entry.name to (entry.codeSigners?.toSet() ?: emptySet()) }
|
||||
|
||||
private fun Set<CodeSigner>.toOrderedPublicKeys(): List<PublicKey> = map {
|
||||
(it.signerCertPath.certificates[0] as X509Certificate).publicKey
|
||||
}.sortedBy { it.hash} // Sorted for determinism.
|
||||
|
||||
private fun Set<CodeSigner>.toPartiesOrderedByName(): List<Party> = map {
|
||||
Party(it.signerCertPath.certificates[0] as X509Certificate)
|
||||
}.sortedBy { it.name.toString() } // Sorted for determinism.
|
||||
|
@ -7,6 +7,7 @@ import net.corda.core.node.services.AttachmentId
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
|
||||
import net.corda.core.utilities.days
|
||||
import java.security.PublicKey
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
@ -22,6 +23,7 @@ import java.time.Instant
|
||||
* of parameters.
|
||||
* @property whitelistedContractImplementations List of whitelisted jars containing contract code for each contract class.
|
||||
* This will be used by [net.corda.core.contracts.WhitelistedByZoneAttachmentConstraint]. [You can learn more about contract constraints here](https://docs.corda.net/api-contract-constraints.html).
|
||||
* @property packageOwnership List of the network-wide java packages that were successfully claimed by their owners. Any CorDapp JAR that offers contracts and states in any of these packages must be signed by the owner.
|
||||
* @property eventHorizon Time after which nodes will be removed from the network map if they have not been seen
|
||||
* during this period
|
||||
*/
|
||||
@ -35,7 +37,8 @@ data class NetworkParameters(
|
||||
val modifiedTime: Instant,
|
||||
val epoch: Int,
|
||||
val whitelistedContractImplementations: Map<String, List<AttachmentId>>,
|
||||
val eventHorizon: Duration
|
||||
val eventHorizon: Duration,
|
||||
val packageOwnership: Map<JavaPackageName, PublicKey>
|
||||
) {
|
||||
@DeprecatedConstructorForDeserialization(1)
|
||||
constructor (minimumPlatformVersion: Int,
|
||||
@ -52,7 +55,28 @@ data class NetworkParameters(
|
||||
modifiedTime,
|
||||
epoch,
|
||||
whitelistedContractImplementations,
|
||||
Int.MAX_VALUE.days
|
||||
Int.MAX_VALUE.days,
|
||||
emptyMap()
|
||||
)
|
||||
|
||||
@DeprecatedConstructorForDeserialization(2)
|
||||
constructor (minimumPlatformVersion: Int,
|
||||
notaries: List<NotaryInfo>,
|
||||
maxMessageSize: Int,
|
||||
maxTransactionSize: Int,
|
||||
modifiedTime: Instant,
|
||||
epoch: Int,
|
||||
whitelistedContractImplementations: Map<String, List<AttachmentId>>,
|
||||
eventHorizon: Duration
|
||||
) : this(minimumPlatformVersion,
|
||||
notaries,
|
||||
maxMessageSize,
|
||||
maxTransactionSize,
|
||||
modifiedTime,
|
||||
epoch,
|
||||
whitelistedContractImplementations,
|
||||
eventHorizon,
|
||||
emptyMap()
|
||||
)
|
||||
|
||||
init {
|
||||
@ -63,6 +87,7 @@ data class NetworkParameters(
|
||||
require(maxTransactionSize > 0) { "maxTransactionSize must be at least 1" }
|
||||
require(maxTransactionSize <= maxMessageSize) { "maxTransactionSize cannot be bigger than maxMessageSize" }
|
||||
require(!eventHorizon.isNegative) { "eventHorizon must be positive value" }
|
||||
require(noOverlap(packageOwnership.keys)) { "multiple packages added to the packageOwnership overlap." }
|
||||
}
|
||||
|
||||
fun copy(minimumPlatformVersion: Int,
|
||||
@ -83,20 +108,47 @@ data class NetworkParameters(
|
||||
eventHorizon = eventHorizon)
|
||||
}
|
||||
|
||||
fun copy(minimumPlatformVersion: Int,
|
||||
notaries: List<NotaryInfo>,
|
||||
maxMessageSize: Int,
|
||||
maxTransactionSize: Int,
|
||||
modifiedTime: Instant,
|
||||
epoch: Int,
|
||||
whitelistedContractImplementations: Map<String, List<AttachmentId>>,
|
||||
eventHorizon: Duration
|
||||
): NetworkParameters {
|
||||
return copy(minimumPlatformVersion = minimumPlatformVersion,
|
||||
notaries = notaries,
|
||||
maxMessageSize = maxMessageSize,
|
||||
maxTransactionSize = maxTransactionSize,
|
||||
modifiedTime = modifiedTime,
|
||||
epoch = epoch,
|
||||
whitelistedContractImplementations = whitelistedContractImplementations,
|
||||
eventHorizon = eventHorizon)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return """NetworkParameters {
|
||||
minimumPlatformVersion=$minimumPlatformVersion
|
||||
notaries=$notaries
|
||||
maxMessageSize=$maxMessageSize
|
||||
maxTransactionSize=$maxTransactionSize
|
||||
whitelistedContractImplementations {
|
||||
${whitelistedContractImplementations.entries.joinToString("\n ")}
|
||||
}
|
||||
eventHorizon=$eventHorizon
|
||||
modifiedTime=$modifiedTime
|
||||
epoch=$epoch
|
||||
}"""
|
||||
minimumPlatformVersion=$minimumPlatformVersion
|
||||
notaries=$notaries
|
||||
maxMessageSize=$maxMessageSize
|
||||
maxTransactionSize=$maxTransactionSize
|
||||
whitelistedContractImplementations {
|
||||
${whitelistedContractImplementations.entries.joinToString("\n ")}
|
||||
}
|
||||
eventHorizon=$eventHorizon
|
||||
modifiedTime=$modifiedTime
|
||||
epoch=$epoch,
|
||||
packageOwnership= {
|
||||
${packageOwnership.keys.joinToString()}}
|
||||
}
|
||||
}"""
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the public key of the package owner of the [contractClassName], or null if not owned.
|
||||
*/
|
||||
fun getOwnerOf(contractClassName: String): PublicKey? = this.packageOwnership.filterKeys { it.owns(contractClassName) }.values.singleOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -113,3 +165,32 @@ data class NotaryInfo(val identity: Party, val validating: Boolean)
|
||||
* version.
|
||||
*/
|
||||
class ZoneVersionTooLowException(message: String) : CordaRuntimeException(message)
|
||||
|
||||
/**
|
||||
* A wrapper for a legal java package. Used by the network parameters to store package ownership.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class JavaPackageName(val name: String) {
|
||||
init {
|
||||
require(isPackageValid(name)) { "Attempting to whitelist illegal java package: $name" }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the [fullClassName] is in a subpackage of the current package.
|
||||
* E.g.: "com.megacorp" owns "com.megacorp.tokens.MegaToken"
|
||||
*
|
||||
* Note: The ownership check is ignoring case to prevent people from just releasing a jar with: "com.megaCorp.megatoken" and pretend they are MegaCorp.
|
||||
* By making the check case insensitive, the node will require that the jar is signed by MegaCorp, so the attack fails.
|
||||
*/
|
||||
fun owns(fullClassName: String) = fullClassName.startsWith("${name}.", ignoreCase = true)
|
||||
}
|
||||
|
||||
// Check if a string is a legal Java package name.
|
||||
private fun isPackageValid(packageName: String): Boolean = packageName.isNotEmpty() && !packageName.endsWith(".") && packageName.split(".").all { token ->
|
||||
Character.isJavaIdentifierStart(token[0]) && token.toCharArray().drop(1).all { Character.isJavaIdentifierPart(it) }
|
||||
}
|
||||
|
||||
// Make sure that packages don't overlap so that ownership is clear.
|
||||
private fun noOverlap(packages: Collection<JavaPackageName>) = packages.all { currentPackage ->
|
||||
packages.none { otherPackage -> otherPackage != currentPackage && otherPackage.name.startsWith("${currentPackage.name}.") }
|
||||
}
|
||||
|
@ -197,7 +197,7 @@ data class ContractUpgradeLedgerTransaction(
|
||||
private fun verifyConstraints() {
|
||||
val attachmentForConstraintVerification = AttachmentWithContext(
|
||||
legacyContractAttachment as? ContractAttachment
|
||||
?: ContractAttachment(legacyContractAttachment, legacyContractClassName),
|
||||
?: ContractAttachment(legacyContractAttachment, legacyContractClassName, signers = legacyContractAttachment.signers),
|
||||
upgradedContract.legacyContract,
|
||||
networkParameters.whitelistedContractImplementations
|
||||
)
|
||||
|
@ -3,6 +3,7 @@ package net.corda.core.transactions
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.AttachmentWithContext
|
||||
import net.corda.core.internal.castIfPossible
|
||||
@ -12,6 +13,7 @@ import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import java.util.*
|
||||
import java.util.function.Predicate
|
||||
import net.corda.core.utilities.warnOnce
|
||||
|
||||
/**
|
||||
* A LedgerTransaction is derived from a [WireTransaction]. It is the result of doing the following operations:
|
||||
@ -60,6 +62,9 @@ data class LedgerTransaction @JvmOverloads constructor(
|
||||
val inputStates: List<ContractState> get() = inputs.map { it.state.data }
|
||||
val referenceStates: List<ContractState> get() = references.map { it.state.data }
|
||||
|
||||
private val inputAndOutputStates = inputs.map { it.state } + outputs
|
||||
private val allStates = inputAndOutputStates + references.map { it.state }
|
||||
|
||||
/**
|
||||
* Returns the typed input StateAndRef at the specified index
|
||||
* @param index The index into the inputs.
|
||||
@ -74,25 +79,28 @@ data class LedgerTransaction @JvmOverloads constructor(
|
||||
*/
|
||||
@Throws(TransactionVerificationException::class)
|
||||
fun verify() {
|
||||
val contractAttachmentsByContract: Map<ContractClassName, ContractAttachment> = getUniqueContractAttachmentsByContract()
|
||||
|
||||
// TODO - verify for version downgrade
|
||||
validatePackageOwnership(contractAttachmentsByContract)
|
||||
validateStatesAgainstContract()
|
||||
verifyConstraints()
|
||||
verifyConstraintsValidity(contractAttachmentsByContract)
|
||||
verifyConstraints(contractAttachmentsByContract)
|
||||
verifyContracts()
|
||||
}
|
||||
|
||||
private fun allStates() = inputs.asSequence().map { it.state } + outputs.asSequence()
|
||||
|
||||
/**
|
||||
* For all input and output [TransactionState]s, validates that the wrapped [ContractState] matches up with the
|
||||
* wrapped [Contract], as declared by the [BelongsToContract] annotation on the [ContractState]'s class.
|
||||
*
|
||||
* A warning will be written to the log if any mismatch is detected.
|
||||
*/
|
||||
private fun validateStatesAgainstContract() = allStates().forEach(::validateStateAgainstContract)
|
||||
private fun validateStatesAgainstContract() = allStates.forEach(::validateStateAgainstContract)
|
||||
|
||||
private fun validateStateAgainstContract(state: TransactionState<ContractState>) {
|
||||
state.data.requiredContractClassName?.let { requiredContractClassName ->
|
||||
if (state.contract != requiredContractClassName)
|
||||
logger.warn("""
|
||||
logger.warnOnce("""
|
||||
State of class ${state.data::class.java.typeName} belongs to contract $requiredContractClassName, but
|
||||
is bundled in TransactionState with ${state.contract}.
|
||||
""".trimIndent().replace('\n', ' '))
|
||||
@ -100,20 +108,74 @@ data class LedgerTransaction @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that all contract constraints are valid for each state before running any contract code
|
||||
* Verify that for each contract the network wide package owner is respected.
|
||||
*
|
||||
* In case the transaction was created on this node then the attachments will contain the hash of the current cordapp jars.
|
||||
* In case this verifies an older transaction or one originated on a different node, then this verifies that the attachments
|
||||
* are valid.
|
||||
* TODO - revisit once transaction contains network parameters.
|
||||
*/
|
||||
private fun validatePackageOwnership(contractAttachmentsByContract: Map<ContractClassName, ContractAttachment>) {
|
||||
// This should never happen once we have network parameters in the transaction.
|
||||
if (networkParameters == null) {
|
||||
return
|
||||
}
|
||||
|
||||
val contractsAndOwners = allStates.mapNotNull { transactionState ->
|
||||
val contractClassName = transactionState.contract
|
||||
networkParameters.getOwnerOf(contractClassName)?.let { contractClassName to it }
|
||||
}.toMap()
|
||||
|
||||
contractsAndOwners.forEach { contract, owner ->
|
||||
val attachment = contractAttachmentsByContract[contract]!!
|
||||
if (!owner.isFulfilledBy(attachment.signers)) {
|
||||
throw TransactionVerificationException.ContractAttachmentNotSignedByPackageOwnerException(this.id, id, contract)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforces the validity of the actual constraints.
|
||||
* * Constraints should be one of the valid supported ones.
|
||||
* * Constraints should propagate correctly if not marked otherwise.
|
||||
*/
|
||||
private fun verifyConstraintsValidity(contractAttachmentsByContract: Map<ContractClassName, ContractAttachment>) {
|
||||
// First check that the constraints are valid.
|
||||
for (state in allStates) {
|
||||
checkConstraintValidity(state)
|
||||
}
|
||||
|
||||
// Group the inputs and outputs by contract, and for each contract verify the constraints propagation logic.
|
||||
// This is not required for reference states as there is nothing to propagate.
|
||||
val inputContractGroups = inputs.groupBy { it.state.contract }
|
||||
val outputContractGroups = outputs.groupBy { it.contract }
|
||||
|
||||
for (contractClassName in (inputContractGroups.keys + outputContractGroups.keys)) {
|
||||
if (contractClassName.contractHasAutomaticConstraintPropagation()) {
|
||||
// Verify that the constraints of output states have at least the same level of restriction as the constraints of the corresponding input states.
|
||||
val inputConstraints = inputContractGroups[contractClassName]?.map { it.state.constraint }?.toSet()
|
||||
val outputConstraints = outputContractGroups[contractClassName]?.map { it.constraint }?.toSet()
|
||||
outputConstraints?.forEach { outputConstraint ->
|
||||
inputConstraints?.forEach { inputConstraint ->
|
||||
if (!(outputConstraint.canBeTransitionedFrom(inputConstraint, contractAttachmentsByContract[contractClassName]!! ))) {
|
||||
throw TransactionVerificationException.ConstraintPropagationRejection(id, contractClassName, inputConstraint, outputConstraint)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
contractClassName.warnContractWithoutConstraintPropagation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that all contract constraints are passing before running any contract code.
|
||||
*
|
||||
* This check is running the [AttachmentConstraint.isSatisfiedBy] method for each corresponding [ContractAttachment].
|
||||
*
|
||||
* @throws TransactionVerificationException if the constraints fail to verify
|
||||
*/
|
||||
private fun verifyConstraints() {
|
||||
val contractAttachmentsByContract = getUniqueContractAttachmentsByContract()
|
||||
|
||||
for (state in allStates()) {
|
||||
val contractAttachment = contractAttachmentsByContract[state.contract] ?:
|
||||
throw TransactionVerificationException.MissingAttachmentRejection(id, state.contract)
|
||||
private fun verifyConstraints(contractAttachmentsByContract: Map<ContractClassName, ContractAttachment>) {
|
||||
for (state in allStates) {
|
||||
val contractAttachment = contractAttachmentsByContract[state.contract]
|
||||
?: throw TransactionVerificationException.MissingAttachmentRejection(id, state.contract)
|
||||
|
||||
val constraintAttachment = AttachmentWithContext(contractAttachment, state.contract,
|
||||
networkParameters?.whitelistedContractImplementations)
|
||||
@ -131,16 +193,16 @@ data class LedgerTransaction @JvmOverloads constructor(
|
||||
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)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -151,7 +213,7 @@ data class LedgerTransaction @JvmOverloads constructor(
|
||||
* Check the transaction is contract-valid by running the verify() for each input and output state contract.
|
||||
* If any contract fails to verify, the whole transaction is considered to be invalid.
|
||||
*/
|
||||
private fun verifyContracts() = allStates().forEach { ts ->
|
||||
private fun verifyContracts() = inputAndOutputStates.forEach { ts ->
|
||||
val contractClass = getContractClass(ts)
|
||||
val contract = createContractInstance(contractClass)
|
||||
|
||||
@ -174,11 +236,11 @@ data class LedgerTransaction @JvmOverloads constructor(
|
||||
|
||||
// Obtain an instance of the contract class, wrapping any exception as a [ContractCreationError]
|
||||
private fun createContractInstance(contractClass: Class<out Contract>): Contract =
|
||||
try {
|
||||
contractClass.newInstance()
|
||||
} catch (e: Exception) {
|
||||
throw TransactionVerificationException.ContractCreationError(id, contractClass.name, e)
|
||||
}
|
||||
try {
|
||||
contractClass.newInstance()
|
||||
} catch (e: Exception) {
|
||||
throw TransactionVerificationException.ContractCreationError(id, contractClass.name, e)
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure the notary has stayed the same. As we can't tell how inputs and outputs connect, if there
|
||||
|
@ -4,11 +4,12 @@ import co.paralleluniverse.strands.Strand
|
||||
import net.corda.core.CordaInternal
|
||||
import net.corda.core.DeleteForDJVM
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.cordapp.CordappProvider
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.AttachmentWithContext
|
||||
import net.corda.core.internal.FlowStateMachine
|
||||
import net.corda.core.internal.ensureMinimumPlatformVersion
|
||||
import net.corda.core.internal.isUploaderTrusted
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
@ -17,6 +18,7 @@ import net.corda.core.node.services.AttachmentId
|
||||
import net.corda.core.node.services.KeyManagementService
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializationFactory
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import java.security.PublicKey
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
@ -47,6 +49,11 @@ open class TransactionBuilder @JvmOverloads constructor(
|
||||
protected var privacySalt: PrivacySalt = PrivacySalt(),
|
||||
protected val references: MutableList<StateRef> = arrayListOf()
|
||||
) {
|
||||
|
||||
private companion object {
|
||||
val logger = loggerFor<TransactionBuilder>()
|
||||
}
|
||||
|
||||
private val inputsWithTransactionState = arrayListOf<TransactionState<ContractState>>()
|
||||
private val referencesWithTransactionState = arrayListOf<TransactionState<ContractState>>()
|
||||
|
||||
@ -91,8 +98,7 @@ open class TransactionBuilder @JvmOverloads constructor(
|
||||
// DOCEND 1
|
||||
|
||||
/**
|
||||
* Generates a [WireTransaction] from this builder and resolves any [AutomaticHashConstraint] on contracts to
|
||||
* [HashAttachmentConstraint].
|
||||
* Generates a [WireTransaction] from this builder, resolves any [AutomaticPlaceholderConstraint], and selects the attachments to use for this transaction.
|
||||
*
|
||||
* @returns A new [WireTransaction] that will be unaffected by further changes to this [TransactionBuilder].
|
||||
*
|
||||
@ -108,21 +114,11 @@ open class TransactionBuilder @JvmOverloads constructor(
|
||||
services.ensureMinimumPlatformVersion(4, "Reference states")
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the [AutomaticHashConstraint]s to [HashAttachmentConstraint]s,
|
||||
* [WhitelistedByZoneAttachmentConstraint]s or [SignatureAttachmentConstraint]s based on a global parameter.
|
||||
*
|
||||
* The [AutomaticHashConstraint] allows for less boiler plate when constructing transactions since for the
|
||||
* typical case the named contract will be available when building the transaction. In exceptional cases the
|
||||
* [TransactionStates] must be created with an explicit [AttachmentConstraint]
|
||||
*/
|
||||
val resolvedOutputs = outputs.map { state ->
|
||||
state.withConstraint(when {
|
||||
state.constraint !== AutomaticHashConstraint -> state.constraint
|
||||
useWhitelistedByZoneAttachmentConstraint(state.contract, services.networkParameters) ->
|
||||
WhitelistedByZoneAttachmentConstraint
|
||||
else -> makeAttachmentConstraint(services, state)
|
||||
})
|
||||
val (allContractAttachments: Collection<SecureHash>, resolvedOutputs: List<TransactionState<ContractState>>) = selectContractAttachmentsAndOutputStateConstraints(services, serializationContext)
|
||||
|
||||
// Final sanity check that all states have the correct constraints.
|
||||
for (state in (inputsWithTransactionState + resolvedOutputs)) {
|
||||
checkConstraintValidity(state)
|
||||
}
|
||||
|
||||
return SerializationFactory.defaultFactory.withCurrentContext(serializationContext) {
|
||||
@ -131,51 +127,250 @@ open class TransactionBuilder @JvmOverloads constructor(
|
||||
inputStates(),
|
||||
resolvedOutputs,
|
||||
commands,
|
||||
attachments + makeContractAttachments(services.cordappProvider),
|
||||
(allContractAttachments + attachments).toSortedSet().toList(), // Sort the attachments to ensure transaction builds are stable.
|
||||
notary,
|
||||
window,
|
||||
referenceStates
|
||||
),
|
||||
referenceStates),
|
||||
privacySalt
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TransactionState<ContractState>.withConstraint(newConstraint: AttachmentConstraint) =
|
||||
if (newConstraint == constraint) this else copy(constraint = newConstraint)
|
||||
/**
|
||||
* This method is responsible for selecting the contract versions to be used for the current transaction and resolve the output state [AutomaticPlaceholderConstraint]s.
|
||||
* The contract attachments are used to create a deterministic Classloader to deserialise the transaction and to run the contract verification.
|
||||
*
|
||||
* The selection logic depends on the Attachment Constraints of the input, output and reference states, also on the explicitly set attachments.
|
||||
* TODO also on the versions of the attachments of the transactions generating the input states. ( after we add versioning)
|
||||
*/
|
||||
private fun selectContractAttachmentsAndOutputStateConstraints(
|
||||
services: ServicesForResolution, serializationContext: SerializationContext?): Pair<Collection<SecureHash>, List<TransactionState<ContractState>>> {
|
||||
|
||||
private fun makeAttachmentConstraint(services: ServicesForResolution, state: TransactionState<ContractState>): AttachmentConstraint {
|
||||
val attachmentId = services.cordappProvider.getContractAttachmentID(state.contract)
|
||||
?: throw MissingContractAttachments(listOf(state))
|
||||
// Determine the explicitly set contract attachments.
|
||||
val explicitAttachmentContracts: List<Pair<ContractClassName, SecureHash>> = this.attachments
|
||||
.map(services.attachments::openAttachment)
|
||||
.mapNotNull { it as? ContractAttachment }
|
||||
.flatMap { attch ->
|
||||
attch.allContracts.map { it to attch.id }
|
||||
}
|
||||
|
||||
val attachmentSigners = services.attachments.openAttachment(attachmentId)?.signers
|
||||
?: throw MissingContractAttachments(listOf(state))
|
||||
// And fail early if there's more than 1 for a contract.
|
||||
require(explicitAttachmentContracts.isEmpty() || explicitAttachmentContracts.groupBy { (ctr, _) -> ctr }.all { (_, groups) -> groups.size == 1 }) { "Multiple attachments set for the same contract." }
|
||||
|
||||
return when {
|
||||
attachmentSigners.isEmpty() -> HashAttachmentConstraint(attachmentId)
|
||||
else -> makeSignatureAttachmentConstraint(attachmentSigners)
|
||||
val explicitAttachmentContractsMap: Map<ContractClassName, SecureHash> = explicitAttachmentContracts.toMap()
|
||||
|
||||
val inputContractGroups: Map<ContractClassName, List<TransactionState<ContractState>>> = inputsWithTransactionState.groupBy { it.contract }
|
||||
val outputContractGroups: Map<ContractClassName, List<TransactionState<ContractState>>> = outputs.groupBy { it.contract }
|
||||
|
||||
val allContracts: Set<ContractClassName> = inputContractGroups.keys + outputContractGroups.keys
|
||||
|
||||
// Handle reference states.
|
||||
// Filter out all contracts that might have been already used by 'normal' input or output states.
|
||||
val referenceStateGroups: Map<ContractClassName, List<TransactionState<ContractState>>> = referencesWithTransactionState.groupBy { it.contract }
|
||||
val refStateContractAttachments: List<AttachmentId> = referenceStateGroups
|
||||
.filterNot { it.key in allContracts }
|
||||
.map { refStateEntry ->
|
||||
selectAttachmentThatSatisfiesConstraints(true, refStateEntry.key, refStateEntry.value.map { it.constraint }, services)
|
||||
}
|
||||
|
||||
// For each contract, resolve the AutomaticPlaceholderConstraint, and select the attachment.
|
||||
val contractAttachmentsAndResolvedOutputStates: List<Pair<AttachmentId, List<TransactionState<ContractState>>?>> = allContracts.toSet().map { ctr ->
|
||||
handleContract(ctr, inputContractGroups[ctr], outputContractGroups[ctr], explicitAttachmentContractsMap[ctr], serializationContext, services)
|
||||
}
|
||||
|
||||
val resolvedStates: List<TransactionState<ContractState>> = contractAttachmentsAndResolvedOutputStates.mapNotNull { it.second }.flatten()
|
||||
|
||||
// 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
|
||||
|
||||
return Pair(attachments, resolvedOutputStatesInTheOriginalOrder)
|
||||
}
|
||||
|
||||
private fun makeSignatureAttachmentConstraint(attachmentSigners: List<Party>) =
|
||||
SignatureAttachmentConstraint(CompositeKey.Builder().addKeys(attachmentSigners.map { it.owningKey }).build())
|
||||
|
||||
private fun useWhitelistedByZoneAttachmentConstraint(contractClassName: ContractClassName, networkParameters: NetworkParameters) =
|
||||
contractClassName in networkParameters.whitelistedContractImplementations.keys
|
||||
private val automaticConstraints = setOf(AutomaticPlaceholderConstraint, AutomaticHashConstraint)
|
||||
|
||||
/**
|
||||
* The attachments added to the current transaction contain only the hashes of the current cordapps.
|
||||
* NOT the hashes of the cordapps that were used when the input states were created ( in case they changed in the meantime)
|
||||
* TODO - review this logic
|
||||
* Selects an attachment and resolves the constraints for the output states with [AutomaticPlaceholderConstraint].
|
||||
*
|
||||
* This is the place where the complex logic of the upgradability of contracts and constraint propagation is handled.
|
||||
*
|
||||
* * For contracts that *are not* annotated with @[NoConstraintPropagation], this will attempt to determine a constraint for the output states
|
||||
* that is a valid transition from all the constraints of the input states.
|
||||
*
|
||||
* * For contracts that *are* annotated with @[NoConstraintPropagation], this enforces setting an explicit output constraint.
|
||||
*
|
||||
* * For states with the [HashAttachmentConstraint], if an attachment with that hash is installed on the current node, then it will be inherited by the output states and selected for the transaction.
|
||||
* Otherwise a [MissingContractAttachments] is thrown.
|
||||
*
|
||||
* * For input states with [WhitelistedByZoneAttachmentConstraint] or a [AlwaysAcceptAttachmentConstraint] implementations, then the currently installed cordapp version is used.
|
||||
*/
|
||||
private fun makeContractAttachments(cordappProvider: CordappProvider): List<AttachmentId> {
|
||||
// Reference inputs not included as it is not necessary to verify them.
|
||||
return (inputsWithTransactionState + outputs).map { state ->
|
||||
cordappProvider.getContractAttachmentID(state.contract)
|
||||
?: throw MissingContractAttachments(listOf(state))
|
||||
}.distinct()
|
||||
private fun handleContract(
|
||||
contractClassName: ContractClassName,
|
||||
inputStates: List<TransactionState<ContractState>>?,
|
||||
outputStates: List<TransactionState<ContractState>>?,
|
||||
explicitContractAttachment: AttachmentId?,
|
||||
serializationContext: SerializationContext?,
|
||||
services: ServicesForResolution
|
||||
): Pair<AttachmentId, List<TransactionState<ContractState>>?> {
|
||||
val inputsAndOutputs = (inputStates ?: emptyList()) + (outputStates ?: emptyList())
|
||||
|
||||
// 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 }
|
||||
.map { state ->
|
||||
val attachment = services.attachments.openAttachment((state.constraint as HashAttachmentConstraint).attachmentId)
|
||||
if (attachment == null || attachment !is ContractAttachment || !isUploaderTrusted(attachment.uploader)) {
|
||||
// This should never happen because these are input states that should have been validated already.
|
||||
throw MissingContractAttachments(listOf(state))
|
||||
}
|
||||
attachment
|
||||
}.toSet()
|
||||
|
||||
// Check that states with the HashConstraint don't conflict between themselves or with an explicitly set attachment.
|
||||
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
|
||||
|
||||
fun selectAttachment() = selectAttachmentThatSatisfiesConstraints(
|
||||
false,
|
||||
contractClassName,
|
||||
inputsAndOutputs.map { it.constraint }.toSet().filterNot { it in automaticConstraints },
|
||||
services)
|
||||
|
||||
// This will contain the hash of the JAR that will be used by this Transaction.
|
||||
val selectedAttachmentId = forcedAttachmentId ?: selectAttachment()
|
||||
|
||||
val attachmentToUse = services.attachments.openAttachment(selectedAttachmentId)?.let { it as ContractAttachment }
|
||||
?: throw IllegalArgumentException("Contract attachment $selectedAttachmentId for $contractClassName is missing.")
|
||||
|
||||
// For Exit transactions (no output states) there is no need to resolve the output constraints.
|
||||
if (outputStates == null) {
|
||||
return Pair(selectedAttachmentId, null)
|
||||
}
|
||||
|
||||
// If there are no automatic constraints, there is nothing to resolve.
|
||||
if (outputStates.none { it.constraint in automaticConstraints }) {
|
||||
return Pair(selectedAttachmentId, outputStates)
|
||||
}
|
||||
|
||||
// The final step is to resolve AutomaticPlaceholderConstraint.
|
||||
val automaticConstraintPropagation = contractClassName.contractHasAutomaticConstraintPropagation(serializationContext?.deserializationClassLoader)
|
||||
|
||||
// When automaticConstraintPropagation is disabled for a contract, output states must an explicit Constraint.
|
||||
require(automaticConstraintPropagation) { "Contract $contractClassName was marked with @NoConstraintPropagation, which means the constraint of the output states has to be set explicitly." }
|
||||
|
||||
// This is the logic to determine the constraint which will replace the AutomaticPlaceholderConstraint.
|
||||
val defaultOutputConstraint = selectAttachmentConstraint(contractClassName, inputStates, attachmentToUse, services)
|
||||
|
||||
// Sanity check that the selected attachment actually passes.
|
||||
val constraintAttachment = AttachmentWithContext(attachmentToUse, contractClassName, services.networkParameters.whitelistedContractImplementations)
|
||||
require(defaultOutputConstraint.isSatisfiedBy(constraintAttachment)) { "Selected output constraint: $defaultOutputConstraint not satisfying $selectedAttachmentId" }
|
||||
|
||||
val resolvedOutputStates = outputStates.map {
|
||||
val outputConstraint = it.constraint
|
||||
if (outputConstraint in automaticConstraints) {
|
||||
it.copy(constraint = defaultOutputConstraint)
|
||||
} 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.isSatisfiedBy(constraintAttachment)) { "Output state constraint check fails. $outputConstraint" }
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
return Pair(selectedAttachmentId, resolvedOutputStates)
|
||||
}
|
||||
|
||||
/**
|
||||
* If there are multiple input states with different constraints then run the constraint intersection logic to determine the resulting output constraint.
|
||||
* For issuing transactions where the attachmentToUse is JarSigned, then default to the SignatureConstraint with all the signatures.
|
||||
* TODO - in the future this step can actually create a new ContractAttachment by merging 2 signed jars of the same version.
|
||||
*/
|
||||
private fun selectAttachmentConstraint(
|
||||
contractClassName: ContractClassName,
|
||||
inputStates: List<TransactionState<ContractState>>?,
|
||||
attachmentToUse: ContractAttachment,
|
||||
services: ServicesForResolution): AttachmentConstraint = when {
|
||||
inputStates != null -> attachmentConstraintsTransition(inputStates.groupBy { it.constraint }.keys, attachmentToUse)
|
||||
useWhitelistedByZoneAttachmentConstraint(contractClassName, services.networkParameters) -> WhitelistedByZoneAttachmentConstraint
|
||||
attachmentToUse.signers.isNotEmpty() -> makeSignatureAttachmentConstraint(attachmentToUse.signers)
|
||||
else -> HashAttachmentConstraint(attachmentToUse.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a set of [AttachmentConstraint]s, this function implements the rules on how constraints can evolve.
|
||||
*
|
||||
* This should be an exhaustive check, and should mirror [AttachmentConstraint.canBeTransitionedFrom].
|
||||
*
|
||||
* TODO - once support for third party signing is added, it should be implemented here. ( a constraint with 2 signatures is less restrictive than a constraint with 1 more signature)
|
||||
*/
|
||||
private fun attachmentConstraintsTransition(constraints: Set<AttachmentConstraint>, attachmentToUse: ContractAttachment): AttachmentConstraint = when {
|
||||
|
||||
// Sanity check.
|
||||
constraints.isEmpty() -> throw IllegalArgumentException("Cannot transition from no constraints.")
|
||||
|
||||
// When all input states have the same constraint.
|
||||
constraints.size == 1 -> constraints.single()
|
||||
|
||||
// Fail when combining the insecure AlwaysAcceptAttachmentConstraint with something else. The size must be at least 2 at this point.
|
||||
constraints.any { it is AlwaysAcceptAttachmentConstraint } ->
|
||||
throw IllegalArgumentException("Can't mix the AlwaysAcceptAttachmentConstraint with a secure constraint in the same transaction. This can be used to hide insecure transitions.")
|
||||
|
||||
// Multiple states with Hash constraints with different hashes. This should not happen as we checked already.
|
||||
constraints.all { it is HashAttachmentConstraint } ->
|
||||
throw IllegalArgumentException("Cannot mix HashConstraints with different hashes in the same transaction.")
|
||||
|
||||
// The HashAttachmentConstraint is the strongest constraint, so it wins when mixed with anything. As long as the actual constraints pass.
|
||||
// TODO - this could change if we decide to introduce a way to gracefully migrate from the Hash Constraint to the Signature Constraint.
|
||||
constraints.any { it is HashAttachmentConstraint } -> constraints.find { it is HashAttachmentConstraint }!!
|
||||
|
||||
// TODO, we don't currently support mixing signature constraints with different signers. This will change once we introduce third party signers.
|
||||
constraints.all { it is SignatureAttachmentConstraint } ->
|
||||
throw IllegalArgumentException("Cannot mix SignatureAttachmentConstraints signed by different parties in the same transaction.")
|
||||
|
||||
// This ensures a smooth migration from the Whitelist Constraint, given that for the transaction to be valid it still has to pass both constraints.
|
||||
// The transition is possible only when the SignatureConstraint contains ALL signers from the attachment.
|
||||
constraints.any { it is SignatureAttachmentConstraint } && constraints.any { it is WhitelistedByZoneAttachmentConstraint } -> {
|
||||
val signatureConstraint = constraints.mapNotNull { it as? SignatureAttachmentConstraint }.single()
|
||||
when {
|
||||
attachmentToUse.signers.isEmpty() -> throw IllegalArgumentException("Cannot mix a state with the WhitelistedByZoneAttachmentConstraint and a state with the SignatureAttachmentConstraint, when the latest attachment is not signed. Please contact your Zone operator.")
|
||||
signatureConstraint.key.keys.containsAll(attachmentToUse.signers) -> signatureConstraint
|
||||
else -> throw IllegalArgumentException("Attempting to transition a WhitelistedByZoneAttachmentConstraint state backed by an attachment signed by multiple parties to a weaker SignatureConstraint that does not require all those signatures. Please contact your Zone operator.")
|
||||
}
|
||||
}
|
||||
|
||||
else -> throw IllegalArgumentException("Unexpected constraints $constraints.")
|
||||
}
|
||||
|
||||
private fun makeSignatureAttachmentConstraint(attachmentSigners: List<PublicKey>) =
|
||||
SignatureAttachmentConstraint(CompositeKey.Builder().addKeys(attachmentSigners.map { it }).build())
|
||||
|
||||
/**
|
||||
* This method should only be called for upgradeable contracts.
|
||||
*
|
||||
* For now we use the currently installed CorDapp version.
|
||||
* TODO - When the SignatureConstraint and contract version logic is in, this will need to query the attachments table and find the latest one that satisfies all constraints.
|
||||
* TODO - select a version of the contract that is no older than the one from the previous transactions.
|
||||
*/
|
||||
private fun selectAttachmentThatSatisfiesConstraints(isReference: Boolean, contractClassName: String, constraints: List<AttachmentConstraint>, services: ServicesForResolution): AttachmentId {
|
||||
require(constraints.none { it in automaticConstraints })
|
||||
require(isReference || constraints.none { it is HashAttachmentConstraint })
|
||||
return services.cordappProvider.getContractAttachmentID(contractClassName)!!
|
||||
}
|
||||
|
||||
private fun useWhitelistedByZoneAttachmentConstraint(contractClassName: ContractClassName, networkParameters: NetworkParameters) = contractClassName in networkParameters.whitelistedContractImplementations.keys
|
||||
|
||||
@Throws(AttachmentResolutionException::class, TransactionResolutionException::class)
|
||||
fun toLedgerTransaction(services: ServiceHub) = toWireTransaction(services).toLedgerTransaction(services)
|
||||
|
||||
@ -258,7 +453,7 @@ open class TransactionBuilder @JvmOverloads constructor(
|
||||
fun addOutputState(
|
||||
state: ContractState,
|
||||
contract: ContractClassName = requireNotNull(state.requiredContractClassName) {
|
||||
//TODO: add link to docsite page, when there is one.
|
||||
//TODO: add link to docsite page, when there is one.
|
||||
"""
|
||||
Unable to infer Contract class name because state class ${state::class.java.name} is not annotated with
|
||||
@BelongsToContract, and does not have an enclosing class which implements Contract. Either annotate ${state::class.java.name}
|
||||
@ -266,7 +461,7 @@ with @BelongsToContract, or supply an explicit contract parameter to addOutputSt
|
||||
""".trimIndent().replace('\n', ' ')
|
||||
},
|
||||
notary: Party, encumbrance: Int? = null,
|
||||
constraint: AttachmentConstraint = AutomaticHashConstraint
|
||||
constraint: AttachmentConstraint = AutomaticPlaceholderConstraint
|
||||
) = addOutputState(TransactionState(state, contract, notary, encumbrance, constraint))
|
||||
|
||||
/** A default notary must be specified during builder construction to use this method */
|
||||
@ -274,14 +469,14 @@ with @BelongsToContract, or supply an explicit contract parameter to addOutputSt
|
||||
fun addOutputState(
|
||||
state: ContractState,
|
||||
contract: ContractClassName = requireNotNull(state.requiredContractClassName) {
|
||||
//TODO: add link to docsite page, when there is one.
|
||||
//TODO: add link to docsite page, when there is one.
|
||||
"""
|
||||
Unable to infer Contract class name because state class ${state::class.java.name} is not annotated with
|
||||
@BelongsToContract, and does not have an enclosing class which implements Contract. Either annotate ${state::class.java.name}
|
||||
with @BelongsToContract, or supply an explicit contract parameter to addOutputState().
|
||||
""".trimIndent().replace('\n', ' ')
|
||||
},
|
||||
constraint: AttachmentConstraint = AutomaticHashConstraint
|
||||
constraint: AttachmentConstraint = AutomaticPlaceholderConstraint
|
||||
) = apply {
|
||||
checkNotNull(notary) {
|
||||
"Need to specify a notary for the state, or set a default one on TransactionBuilder initialisation"
|
||||
|
@ -9,6 +9,7 @@ import net.corda.core.serialization.CordaSerializable
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.time.Duration
|
||||
import java.util.*
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.Future
|
||||
import kotlin.reflect.KProperty
|
||||
@ -134,3 +135,20 @@ fun <V> Future<V>.getOrThrow(timeout: Duration? = null): V = try {
|
||||
} catch (e: ExecutionException) {
|
||||
throw e.cause!!
|
||||
}
|
||||
|
||||
private const val MAX_SIZE = 100
|
||||
private val warnings = Collections.newSetFromMap(object : LinkedHashMap<String, Boolean>() {
|
||||
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<String, Boolean>?) = size > MAX_SIZE
|
||||
})
|
||||
|
||||
/**
|
||||
* Utility to help log a warning message only once.
|
||||
* It implements an ad hoc Fifo cache because there's none available in the standard libraries.
|
||||
*/
|
||||
@Synchronized
|
||||
fun Logger.warnOnce(warning: String) {
|
||||
if (warning !in warnings) {
|
||||
warnings.add(warning)
|
||||
this.warn(warning)
|
||||
}
|
||||
}
|
45
core/src/test/kotlin/net/corda/core/JarSignatureTestUtils.kt
Normal file
45
core/src/test/kotlin/net/corda/core/JarSignatureTestUtils.kt
Normal file
@ -0,0 +1,45 @@
|
||||
package net.corda.core
|
||||
|
||||
import net.corda.core.internal.JarSignatureCollector
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.nodeapi.internal.crypto.loadKeyStore
|
||||
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, password: String, name: String, keyalg: String = "RSA") =
|
||||
executeProcess("keytool", "-genkey", "-keystore", "_teststore", "-storepass", "storepass", "-keyalg", keyalg, "-alias", alias, "-keypass", password, "-dname", name)
|
||||
|
||||
fun Path.createJar(fileName: String, vararg contents: String) =
|
||||
executeProcess(*(arrayOf("jar", "cvf", fileName) + contents))
|
||||
|
||||
fun Path.updateJar(fileName: String, vararg contents: String) =
|
||||
executeProcess(*(arrayOf("jar", "uvf", fileName) + contents))
|
||||
|
||||
fun Path.signJar(fileName: String, alias: String, password: String): PublicKey {
|
||||
executeProcess("jarsigner", "-keystore", "_teststore", "-storepass", "storepass", "-keypass", password, fileName, alias)
|
||||
val ks = loadKeyStore(this.resolve("_teststore"), "storepass")
|
||||
return ks.getCertificate(alias).publicKey
|
||||
}
|
||||
|
||||
fun Path.getJarSigners(fileName: String) =
|
||||
JarInputStream(FileInputStream((this / fileName).toFile())).use(JarSignatureCollector::collectSigners)
|
||||
}
|
@ -1,8 +1,13 @@
|
||||
package net.corda.core
|
||||
|
||||
import com.nhaarman.mockito_kotlin.mock
|
||||
import com.nhaarman.mockito_kotlin.times
|
||||
import com.nhaarman.mockito_kotlin.verify
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.warnOnce
|
||||
import org.assertj.core.api.Assertions.*
|
||||
import org.junit.Test
|
||||
import org.slf4j.Logger
|
||||
import rx.subjects.PublishSubject
|
||||
import java.util.*
|
||||
import java.util.concurrent.CancellationException
|
||||
@ -58,4 +63,24 @@ class UtilsTest {
|
||||
future.get()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `warnOnce works, but the backing cache grows only to a maximum size`() {
|
||||
val MAX_SIZE = 100
|
||||
|
||||
val logger = mock<Logger>()
|
||||
logger.warnOnce("a")
|
||||
logger.warnOnce("b")
|
||||
logger.warnOnce("b")
|
||||
|
||||
// This should cause the eviction of "a".
|
||||
(1..MAX_SIZE).forEach { logger.warnOnce("$it") }
|
||||
logger.warnOnce("a")
|
||||
|
||||
// "a" should be logged twice because it was evicted.
|
||||
verify(logger, times(2)).warn("a")
|
||||
|
||||
// "b" should be logged only once because there was no eviction.
|
||||
verify(logger, times(1)).warn("b")
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,277 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
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.identity.AbstractParty
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.MissingContractAttachments
|
||||
import net.corda.finance.POUNDS
|
||||
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.internal.rigorousMock
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.ledger
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class ConstraintsPropagationTests {
|
||||
|
||||
private companion object {
|
||||
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
|
||||
val ALICE = TestIdentity(CordaX500Name("ALICE", "London", "GB"))
|
||||
val ALICE_PARTY get() = ALICE.party
|
||||
val ALICE_PUBKEY get() = ALICE.publicKey
|
||||
val BOB = TestIdentity(CordaX500Name("BOB", "London", "GB"))
|
||||
val BOB_PARTY get() = BOB.party
|
||||
val BOB_PUBKEY get() = BOB.publicKey
|
||||
val noPropagationContractClassName = "net.corda.core.contracts.NoPropagationContract"
|
||||
}
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule()
|
||||
|
||||
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)
|
||||
.copy(whitelistedContractImplementations = mapOf(
|
||||
Cash.PROGRAM_ID to listOf(SecureHash.zeroHash, SecureHash.allOnesHash),
|
||||
noPropagationContractClassName to listOf(SecureHash.zeroHash)))
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `Happy path with the HashConstraint`() {
|
||||
ledgerServices.ledger(DUMMY_NOTARY) {
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.allOnesHash)
|
||||
output(Cash.PROGRAM_ID, "c1", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.allOnesHash), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), ALICE_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Issue())
|
||||
verifies()
|
||||
}
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.allOnesHash)
|
||||
input("c1")
|
||||
output(Cash.PROGRAM_ID, "c2", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.allOnesHash), 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) {
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.zeroHash)
|
||||
output(Cash.PROGRAM_ID, "c1", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.zeroHash), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), ALICE_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Issue())
|
||||
verifies()
|
||||
}
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.allOnesHash)
|
||||
input("c1")
|
||||
output(Cash.PROGRAM_ID, "c2", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.allOnesHash), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Transaction validation fails, when constraints do not propagate correctly`() {
|
||||
ledgerServices.ledger(DUMMY_NOTARY) {
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.zeroHash)
|
||||
output(Cash.PROGRAM_ID, "c1", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.zeroHash), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), ALICE_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Issue())
|
||||
verifies()
|
||||
}
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.zeroHash)
|
||||
input("c1")
|
||||
output(Cash.PROGRAM_ID, "c2", DUMMY_NOTARY, null, WhitelistedByZoneAttachmentConstraint, Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
failsWith("are not propagated correctly")
|
||||
}
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.zeroHash)
|
||||
input("c1")
|
||||
output(Cash.PROGRAM_ID, "c3", DUMMY_NOTARY, null, SignatureAttachmentConstraint(ALICE_PUBKEY), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
failsWith("are not propagated correctly")
|
||||
}
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.zeroHash)
|
||||
input("c1")
|
||||
output(Cash.PROGRAM_ID, "c4", DUMMY_NOTARY, null, AlwaysAcceptAttachmentConstraint, Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
failsWith("are not propagated correctly")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `When the constraint of the output state is a valid transition from the input state, transaction validation works`() {
|
||||
ledgerServices.ledger(DUMMY_NOTARY) {
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.zeroHash)
|
||||
output(Cash.PROGRAM_ID, "c1", DUMMY_NOTARY, null, WhitelistedByZoneAttachmentConstraint, Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), ALICE_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Issue())
|
||||
verifies()
|
||||
}
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.zeroHash)
|
||||
input("c1")
|
||||
output(Cash.PROGRAM_ID, "c2", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.zeroHash), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Switching from the WhitelistConstraint to the Signature Constraint is possible if the attachment satisfies both constraints, and the signature constraint inherits all jar signatures`() {
|
||||
|
||||
ledgerServices.ledger(DUMMY_NOTARY) {
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.zeroHash)
|
||||
output(Cash.PROGRAM_ID, "w1", DUMMY_NOTARY, null, WhitelistedByZoneAttachmentConstraint, Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), ALICE_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Issue())
|
||||
verifies()
|
||||
}
|
||||
|
||||
// the attachment is signed
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.allOnesHash, listOf(ALICE_PARTY.owningKey))
|
||||
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))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Switching from the WhitelistConstraint to the Signature Constraint fails if the signature constraint does not inherit all jar signatures`() {
|
||||
ledgerServices.ledger(DUMMY_NOTARY) {
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.zeroHash)
|
||||
output(Cash.PROGRAM_ID, "w1", DUMMY_NOTARY, null, WhitelistedByZoneAttachmentConstraint, Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), ALICE_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Issue())
|
||||
verifies()
|
||||
}
|
||||
|
||||
// the attachment is not signed
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.zeroHash)
|
||||
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))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
// Note that it fails after the constraints propagation check, because the attachment is not signed.
|
||||
failsWith("are not propagated correctly")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `On contract annotated with NoConstraintPropagation there is no platform check for propagation, but the transaction builder can't use the AutomaticPlaceholderConstraint`() {
|
||||
ledgerServices.ledger(DUMMY_NOTARY) {
|
||||
transaction {
|
||||
attachment(noPropagationContractClassName, SecureHash.zeroHash)
|
||||
output(noPropagationContractClassName, "c1", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.zeroHash), NoPropagationContractState())
|
||||
command(ALICE_PUBKEY, NoPropagationContract.Create())
|
||||
verifies()
|
||||
}
|
||||
transaction {
|
||||
attachment(noPropagationContractClassName, SecureHash.zeroHash)
|
||||
input("c1")
|
||||
output(noPropagationContractClassName, "c2", DUMMY_NOTARY, null, WhitelistedByZoneAttachmentConstraint, NoPropagationContractState())
|
||||
command(ALICE_PUBKEY, NoPropagationContract.Create())
|
||||
verifies()
|
||||
}
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
transaction {
|
||||
attachment(noPropagationContractClassName, SecureHash.zeroHash)
|
||||
input("c1")
|
||||
output(noPropagationContractClassName, "c3", DUMMY_NOTARY, null, AutomaticPlaceholderConstraint, NoPropagationContractState())
|
||||
command(ALICE_PUBKEY, NoPropagationContract.Create())
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Attachment canBeTransitionedFrom behaves as expected`() {
|
||||
|
||||
val attachment = mock<ContractAttachment>()
|
||||
whenever(attachment.signers).thenReturn(listOf(ALICE_PARTY.owningKey))
|
||||
|
||||
// Exhaustive positive check
|
||||
assertTrue(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment))
|
||||
assertTrue(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment))
|
||||
|
||||
assertTrue(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment))
|
||||
assertTrue(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment))
|
||||
|
||||
assertTrue(WhitelistedByZoneAttachmentConstraint.canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment))
|
||||
|
||||
assertTrue(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment))
|
||||
|
||||
// Exhaustive negative check
|
||||
assertFalse(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment))
|
||||
assertFalse(WhitelistedByZoneAttachmentConstraint.canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment))
|
||||
assertFalse(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment))
|
||||
|
||||
assertFalse(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment))
|
||||
|
||||
assertFalse(WhitelistedByZoneAttachmentConstraint.canBeTransitionedFrom(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment))
|
||||
assertFalse(WhitelistedByZoneAttachmentConstraint.canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment))
|
||||
|
||||
assertFalse(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment))
|
||||
assertFalse(SignatureAttachmentConstraint(BOB_PUBKEY).canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment))
|
||||
assertFalse(SignatureAttachmentConstraint(BOB_PUBKEY).canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment))
|
||||
|
||||
assertFalse(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment))
|
||||
assertFalse(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment))
|
||||
assertFalse(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment))
|
||||
|
||||
// Fail when encounter a AutomaticPlaceholderConstraint
|
||||
assertFailsWith<IllegalArgumentException> { HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(AutomaticPlaceholderConstraint, attachment) }
|
||||
assertFailsWith<IllegalArgumentException> { AutomaticPlaceholderConstraint.canBeTransitionedFrom(AutomaticPlaceholderConstraint, attachment) }
|
||||
}
|
||||
}
|
||||
|
||||
@BelongsToContract(NoPropagationContract::class)
|
||||
class NoPropagationContractState : ContractState {
|
||||
override val participants: List<AbstractParty>
|
||||
get() = emptyList()
|
||||
}
|
||||
|
||||
@NoConstraintPropagation
|
||||
class NoPropagationContract : Contract {
|
||||
interface Commands : CommandData
|
||||
class Create : Commands
|
||||
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
//do nothing
|
||||
}
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.node.JavaPackageName
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.finance.POUNDS
|
||||
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.internal.rigorousMock
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.ledger
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class PackageOwnershipVerificationTests {
|
||||
|
||||
private companion object {
|
||||
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
|
||||
val ALICE = TestIdentity(CordaX500Name("ALICE", "London", "GB"))
|
||||
val ALICE_PARTY get() = ALICE.party
|
||||
val ALICE_PUBKEY get() = ALICE.publicKey
|
||||
val BOB = TestIdentity(CordaX500Name("BOB", "London", "GB"))
|
||||
val BOB_PARTY get() = BOB.party
|
||||
val BOB_PUBKEY get() = BOB.publicKey
|
||||
val dummyContract = "net.corda.core.contracts.DummyContract"
|
||||
val OWNER_KEY_PAIR = Crypto.generateKeyPair()
|
||||
}
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule()
|
||||
|
||||
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()
|
||||
.copy(packageOwnership = mapOf(JavaPackageName("net.corda.core.contracts") to OWNER_KEY_PAIR.public))
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `Happy path - Transaction validates when package signed by owner`() {
|
||||
ledgerServices.ledger(DUMMY_NOTARY) {
|
||||
transaction {
|
||||
attachment(dummyContract, SecureHash.allOnesHash, listOf(OWNER_KEY_PAIR.public))
|
||||
output(dummyContract, "c1", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.allOnesHash), DummyContractState())
|
||||
command(ALICE_PUBKEY, DummyIssue())
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Transaction validation fails when the selected attachment is not signed by the owner`() {
|
||||
ledgerServices.ledger(DUMMY_NOTARY) {
|
||||
transaction {
|
||||
attachment(dummyContract, SecureHash.allOnesHash, listOf(ALICE_PUBKEY))
|
||||
output(dummyContract, "c1", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.allOnesHash), DummyContractState())
|
||||
command(ALICE_PUBKEY, DummyIssue())
|
||||
failsWith("is not signed by the owner specified in the network parameters")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@BelongsToContract(DummyContract::class)
|
||||
class DummyContractState : ContractState {
|
||||
override val participants: List<AbstractParty>
|
||||
get() = emptyList()
|
||||
}
|
||||
|
||||
class DummyContract : Contract {
|
||||
interface Commands : CommandData
|
||||
class Create : Commands
|
||||
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
//do nothing
|
||||
}
|
||||
}
|
||||
|
||||
class DummyIssue : TypeOnlyCommandData()
|
@ -1,6 +1,10 @@
|
||||
package net.corda.core.internal
|
||||
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.JarSignatureTestUtils.createJar
|
||||
import net.corda.core.JarSignatureTestUtils.generateKey
|
||||
import net.corda.core.JarSignatureTestUtils.getJarSigners
|
||||
import net.corda.core.JarSignatureTestUtils.signJar
|
||||
import net.corda.core.JarSignatureTestUtils.updateJar
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
@ -10,29 +14,14 @@ import org.junit.After
|
||||
import org.junit.AfterClass
|
||||
import org.junit.BeforeClass
|
||||
import org.junit.Test
|
||||
import java.io.FileInputStream
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.util.jar.JarInputStream
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class JarSignatureCollectorTest {
|
||||
companion object {
|
||||
private val dir = Files.createTempDirectory(JarSignatureCollectorTest::class.simpleName)
|
||||
private val bin = Paths.get(System.getProperty("java.home")).let { if (it.endsWith("jre")) it.parent else it } / "bin"
|
||||
private val shredder = (dir / "_shredder").toFile() // No need to delete after each test.
|
||||
|
||||
fun execute(vararg command: String) {
|
||||
assertEquals(0, ProcessBuilder()
|
||||
.inheritIO()
|
||||
.redirectOutput(shredder)
|
||||
.directory(dir.toFile())
|
||||
.command((bin / command[0]).toString(), *command.sliceArray(1 until command.size))
|
||||
.start()
|
||||
.waitFor())
|
||||
}
|
||||
|
||||
private const val FILENAME = "attachment.jar"
|
||||
private const val ALICE = "alice"
|
||||
@ -42,15 +31,12 @@ class JarSignatureCollectorTest {
|
||||
private const val CHARLIE = "Charlie"
|
||||
private const val CHARLIE_PASS = "charliepass"
|
||||
|
||||
private fun generateKey(alias: String, password: String, name: CordaX500Name, keyalg: String = "RSA") =
|
||||
execute("keytool", "-genkey", "-keystore", "_teststore", "-storepass", "storepass", "-keyalg", keyalg, "-alias", alias, "-keypass", password, "-dname", name.toString())
|
||||
|
||||
@BeforeClass
|
||||
@JvmStatic
|
||||
fun beforeClass() {
|
||||
generateKey(ALICE, ALICE_PASS, ALICE_NAME)
|
||||
generateKey(BOB, BOB_PASS, BOB_NAME)
|
||||
generateKey(CHARLIE, CHARLIE_PASS, CHARLIE_NAME, "EC")
|
||||
dir.generateKey(ALICE, ALICE_PASS, ALICE_NAME.toString())
|
||||
dir.generateKey(BOB, BOB_PASS, BOB_NAME.toString())
|
||||
dir.generateKey(CHARLIE, CHARLIE_PASS, CHARLIE_NAME.toString(), "EC")
|
||||
|
||||
(dir / "_signable1").writeLines(listOf("signable1"))
|
||||
(dir / "_signable2").writeLines(listOf("signable2"))
|
||||
@ -64,7 +50,7 @@ class JarSignatureCollectorTest {
|
||||
}
|
||||
}
|
||||
|
||||
private val List<Party>.names get() = map { it.name }
|
||||
private val List<Party>.keys get() = map { it.owningKey }
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
@ -77,101 +63,86 @@ class JarSignatureCollectorTest {
|
||||
@Test
|
||||
fun `empty jar has no signers`() {
|
||||
(dir / "META-INF").createDirectory() // At least one arg is required, and jar cvf conveniently ignores this.
|
||||
createJar("META-INF")
|
||||
assertEquals(emptyList(), getJarSigners())
|
||||
dir.createJar(FILENAME, "META-INF")
|
||||
assertEquals(emptyList(), dir.getJarSigners(FILENAME))
|
||||
|
||||
signAsAlice()
|
||||
assertEquals(emptyList(), getJarSigners()) // There needs to have been a file for ALICE to sign.
|
||||
assertEquals(emptyList(), dir.getJarSigners(FILENAME)) // There needs to have been a file for ALICE to sign.
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unsigned jar has no signers`() {
|
||||
createJar("_signable1")
|
||||
assertEquals(emptyList(), getJarSigners())
|
||||
dir.createJar(FILENAME, "_signable1")
|
||||
assertEquals(emptyList(), dir.getJarSigners(FILENAME))
|
||||
|
||||
updateJar("_signable2")
|
||||
assertEquals(emptyList(), getJarSigners())
|
||||
dir.updateJar(FILENAME, "_signable2")
|
||||
assertEquals(emptyList(), dir.getJarSigners(FILENAME))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `one signer`() {
|
||||
createJar("_signable1", "_signable2")
|
||||
signAsAlice()
|
||||
assertEquals(listOf(ALICE_NAME), getJarSigners().names) // We only reused ALICE's distinguished name, so the keys will be different.
|
||||
dir.createJar(FILENAME, "_signable1", "_signable2")
|
||||
val key = signAsAlice()
|
||||
assertEquals(listOf(key), dir.getJarSigners(FILENAME))
|
||||
|
||||
(dir / "my-dir").createDirectory()
|
||||
updateJar("my-dir")
|
||||
assertEquals(listOf(ALICE_NAME), getJarSigners().names) // Unsigned directory is irrelevant.
|
||||
dir.updateJar(FILENAME, "my-dir")
|
||||
assertEquals(listOf(key), dir.getJarSigners(FILENAME)) // Unsigned directory is irrelevant.
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `two signers`() {
|
||||
createJar("_signable1", "_signable2")
|
||||
signAsAlice()
|
||||
signAsBob()
|
||||
dir.createJar(FILENAME, "_signable1", "_signable2")
|
||||
val key1 = signAsAlice()
|
||||
val key2 = signAsBob()
|
||||
|
||||
assertEquals(listOf(ALICE_NAME, BOB_NAME), getJarSigners().names)
|
||||
assertEquals(setOf(key1, key2), dir.getJarSigners(FILENAME).toSet())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `all files must be signed by the same set of signers`() {
|
||||
createJar("_signable1")
|
||||
signAsAlice()
|
||||
assertEquals(listOf(ALICE_NAME), getJarSigners().names)
|
||||
dir.createJar(FILENAME, "_signable1")
|
||||
val key1 = signAsAlice()
|
||||
assertEquals(listOf(key1), dir.getJarSigners(FILENAME))
|
||||
|
||||
updateJar("_signable2")
|
||||
dir.updateJar(FILENAME, "_signable2")
|
||||
signAsBob()
|
||||
assertFailsWith<InvalidJarSignersException>(
|
||||
"""
|
||||
"""
|
||||
Mismatch between signers [O=Alice Corp, L=Madrid, C=ES, O=Bob Plc, L=Rome, C=IT] for file _signable1
|
||||
and signers [O=Bob Plc, L=Rome, C=IT] for file _signable2.
|
||||
See https://docs.corda.net/design/data-model-upgrades/signature-constraints.html for details of the
|
||||
constraints applied to attachment signatures.
|
||||
""".trimIndent().replace('\n', ' ')
|
||||
) { getJarSigners() }
|
||||
) { dir.getJarSigners(FILENAME) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `bad signature is caught even if the party would not qualify as a signer`() {
|
||||
(dir / "volatile").writeLines(listOf("volatile"))
|
||||
createJar("volatile")
|
||||
signAsAlice()
|
||||
assertEquals(listOf(ALICE_NAME), getJarSigners().names)
|
||||
dir.createJar(FILENAME, "volatile")
|
||||
val key1 = signAsAlice()
|
||||
assertEquals(listOf(key1), dir.getJarSigners(FILENAME))
|
||||
|
||||
(dir / "volatile").writeLines(listOf("garbage"))
|
||||
updateJar("volatile", "_signable1") // ALICE's signature on volatile is now bad.
|
||||
dir.updateJar(FILENAME, "volatile", "_signable1") // ALICE's signature on volatile is now bad.
|
||||
signAsBob()
|
||||
// The JDK doesn't care that BOB has correctly signed the whole thing, it won't let us process the entry with ALICE's bad signature:
|
||||
assertFailsWith<SecurityException> { getJarSigners() }
|
||||
assertFailsWith<SecurityException> { dir.getJarSigners(FILENAME) }
|
||||
}
|
||||
|
||||
// Signing using EC algorithm produced JAR File spec incompatible signature block (META-INF/*.EC) which is anyway accepted by jarsiner, see [JarSignatureCollector]
|
||||
@Test
|
||||
fun `one signer with EC sign algorithm`() {
|
||||
createJar("_signable1", "_signable2")
|
||||
signJar(CHARLIE, CHARLIE_PASS)
|
||||
assertEquals(listOf(CHARLIE_NAME), getJarSigners().names) // We only reused CHARLIE's distinguished name, so the keys will be different.
|
||||
dir.createJar(FILENAME, "_signable1", "_signable2")
|
||||
val charlieKey = dir.signJar(FILENAME, CHARLIE, CHARLIE_PASS)
|
||||
assertEquals(setOf(charlieKey), dir.getJarSigners(FILENAME).toSet()) // We only reused CHARLIE's distinguished name, so the keys will be different.
|
||||
|
||||
(dir / "my-dir").createDirectory()
|
||||
updateJar("my-dir")
|
||||
assertEquals(listOf(CHARLIE_NAME), getJarSigners().names) // Unsigned directory is irrelevant.
|
||||
dir.updateJar(FILENAME, "my-dir")
|
||||
assertEquals(setOf(charlieKey), dir.getJarSigners(FILENAME).toSet()) // Unsigned directory is irrelevant.
|
||||
}
|
||||
|
||||
//region Helper functions
|
||||
private fun createJar(vararg contents: String) =
|
||||
execute(*(arrayOf("jar", "cvf", FILENAME) + contents))
|
||||
|
||||
private fun updateJar(vararg contents: String) =
|
||||
execute(*(arrayOf("jar", "uvf", FILENAME) + contents))
|
||||
|
||||
private fun signJar(alias: String, password: String) =
|
||||
execute("jarsigner", "-keystore", "_teststore", "-storepass", "storepass", "-keypass", password, FILENAME, alias)
|
||||
|
||||
private fun signAsAlice() = signJar(ALICE, ALICE_PASS)
|
||||
private fun signAsBob() = signJar(BOB, BOB_PASS)
|
||||
|
||||
private fun getJarSigners() =
|
||||
JarInputStream(FileInputStream((dir / FILENAME).toFile())).use(JarSignatureCollector::collectSigningParties)
|
||||
//endregion
|
||||
|
||||
private fun signAsAlice() = dir.signJar(FILENAME, ALICE, ALICE_PASS)
|
||||
private fun signAsBob() = dir.signJar(FILENAME, BOB, BOB_PASS)
|
||||
}
|
||||
|
@ -35,11 +35,11 @@ class VaultUpdateTests {
|
||||
private val stateRef3 = StateRef(SecureHash.randomSHA256(), 3)
|
||||
private val stateRef4 = StateRef(SecureHash.randomSHA256(), 4)
|
||||
|
||||
private val stateAndRef0 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY), stateRef0)
|
||||
private val stateAndRef1 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY), stateRef1)
|
||||
private val stateAndRef2 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY), stateRef2)
|
||||
private val stateAndRef3 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY), stateRef3)
|
||||
private val stateAndRef4 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY), stateRef4)
|
||||
private val stateAndRef0 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint), stateRef0)
|
||||
private val stateAndRef1 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint), stateRef1)
|
||||
private val stateAndRef2 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint), stateRef2)
|
||||
private val stateAndRef3 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint), stateRef3)
|
||||
private val stateAndRef4 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint), stateRef4)
|
||||
|
||||
@Test
|
||||
fun `nothing plus nothing is nothing`() {
|
||||
|
@ -63,7 +63,7 @@ class TransactionSerializationTests {
|
||||
// It refers to a fake TX/state that we don't bother creating here.
|
||||
val depositRef = MINI_CORP.ref(1)
|
||||
val fakeStateRef = generateStateRef()
|
||||
val inputState = StateAndRef(TransactionState(TestCash.State(depositRef, 100.POUNDS, MEGA_CORP), TEST_CASH_PROGRAM_ID, DUMMY_NOTARY), fakeStateRef)
|
||||
val inputState = StateAndRef(TransactionState(TestCash.State(depositRef, 100.POUNDS, MEGA_CORP), TEST_CASH_PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint), fakeStateRef )
|
||||
val outputState = TransactionState(TestCash.State(depositRef, 600.POUNDS, MEGA_CORP), TEST_CASH_PROGRAM_ID, DUMMY_NOTARY)
|
||||
val changeState = TransactionState(TestCash.State(depositRef, 400.POUNDS, MEGA_CORP), TEST_CASH_PROGRAM_ID, DUMMY_NOTARY)
|
||||
val megaCorpServices = MockServices(listOf("net.corda.core.serialization"), MEGA_CORP.name, rigorousMock(), MEGA_CORP_KEY)
|
||||
|
@ -73,7 +73,7 @@ class LedgerTransactionQueryTests {
|
||||
)
|
||||
services.recordTransactions(fakeIssueTx)
|
||||
val dummyStateRef = StateRef(fakeIssueTx.id, 0)
|
||||
return StateAndRef(TransactionState(dummyState, DummyContract.PROGRAM_ID, DUMMY_NOTARY, null), dummyStateRef)
|
||||
return StateAndRef(TransactionState(dummyState, DummyContract.PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint), dummyStateRef)
|
||||
}
|
||||
|
||||
private fun makeDummyTransaction(): LedgerTransaction {
|
||||
|
@ -187,7 +187,7 @@ class ReferenceStateTests {
|
||||
@Test
|
||||
fun `state ref cannot be a reference input and regular input in the same transaction`() {
|
||||
val state = ExampleState(ALICE_PARTY, "HELLO CORDA")
|
||||
val stateAndRef = StateAndRef(TransactionState(state, CONTRACT_ID, DUMMY_NOTARY), StateRef(SecureHash.zeroHash, 0))
|
||||
val stateAndRef = StateAndRef(TransactionState(state, CONTRACT_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint), StateRef(SecureHash.zeroHash, 0))
|
||||
assertFailsWith(IllegalArgumentException::class, "A StateRef cannot be both an input and a reference input in the same transaction.") {
|
||||
@Suppress("DEPRECATION") // To be removed when feature is finalised.
|
||||
TransactionBuilder(notary = DUMMY_NOTARY).addInputState(stateAndRef).addReferenceState(stateAndRef.referenced())
|
||||
|
@ -23,6 +23,7 @@ import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.security.PublicKey
|
||||
|
||||
class TransactionBuilderTest {
|
||||
@Rule
|
||||
@ -40,7 +41,15 @@ class TransactionBuilderTest {
|
||||
doReturn(cordappProvider).whenever(services).cordappProvider
|
||||
doReturn(contractAttachmentId).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID)
|
||||
doReturn(testNetworkParameters()).whenever(services).networkParameters
|
||||
doReturn(attachments).whenever(services).attachments
|
||||
|
||||
val attachmentStorage = rigorousMock<AttachmentStorage>()
|
||||
doReturn(attachmentStorage).whenever(services).attachments
|
||||
val attachment = rigorousMock<ContractAttachment>()
|
||||
doReturn(attachment).whenever(attachmentStorage).openAttachment(contractAttachmentId)
|
||||
doReturn(contractAttachmentId).whenever(attachment).id
|
||||
doReturn(setOf(DummyContract.PROGRAM_ID)).whenever(attachment).allContracts
|
||||
doReturn("app").whenever(attachment).uploader
|
||||
doReturn(emptyList<Party>()).whenever(attachment).signers
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -103,6 +112,7 @@ class TransactionBuilderTest {
|
||||
assertTrue(expectedConstraint.isSatisfiedBy(signedAttachment))
|
||||
assertFalse(expectedConstraint.isSatisfiedBy(unsignedAttachment))
|
||||
|
||||
doReturn(attachments).whenever(services).attachments
|
||||
doReturn(signedAttachment).whenever(attachments).openAttachment(contractAttachmentId)
|
||||
|
||||
val outputState = TransactionState(data = DummyState(), contract = DummyContract.PROGRAM_ID, notary = notary)
|
||||
@ -112,19 +122,17 @@ class TransactionBuilderTest {
|
||||
val wtx = builder.toWireTransaction(services)
|
||||
|
||||
assertThat(wtx.outputs).containsOnly(outputState.copy(constraint = expectedConstraint))
|
||||
|
||||
}
|
||||
|
||||
|
||||
private val unsignedAttachment = object : AbstractAttachment({ byteArrayOf() }) {
|
||||
private val unsignedAttachment = ContractAttachment(object : AbstractAttachment({ byteArrayOf() }) {
|
||||
override val id: SecureHash get() = throw UnsupportedOperationException()
|
||||
|
||||
override val signers: List<Party> get() = emptyList()
|
||||
}
|
||||
override val signers: List<PublicKey> get() = emptyList()
|
||||
}, DummyContract.PROGRAM_ID)
|
||||
|
||||
private fun signedAttachment(vararg parties: Party) = object : AbstractAttachment({ byteArrayOf() }) {
|
||||
private fun signedAttachment(vararg parties: Party) = ContractAttachment(object : AbstractAttachment({ byteArrayOf() }) {
|
||||
override val id: SecureHash get() = throw UnsupportedOperationException()
|
||||
|
||||
override val signers: List<Party> get() = parties.toList()
|
||||
}
|
||||
override val signers: List<PublicKey> get() = parties.map { it.owningKey }
|
||||
}, DummyContract.PROGRAM_ID, signers = parties.map { it.owningKey })
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.testing.common.internal.testNetworkParameters
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.testing.internal.createWireTransaction
|
||||
@ -129,7 +130,8 @@ class TransactionTests {
|
||||
id,
|
||||
null,
|
||||
timeWindow,
|
||||
privacySalt
|
||||
privacySalt,
|
||||
testNetworkParameters()
|
||||
)
|
||||
|
||||
transaction.verify()
|
||||
|
@ -19,22 +19,31 @@ isn't allowed unless you're a cash issuer - otherwise you could print money for
|
||||
For a transaction to be valid, the ``verify`` function associated with each state must run successfully. However,
|
||||
for this to be secure, it is not sufficient to specify the ``verify`` function by name as there may exist multiple
|
||||
different implementations with the same method signature and enclosing class. This normally will happen as applications
|
||||
evolve, but could also happen maliciously.
|
||||
evolve, but could also happen maliciously as anyone can create a JAR with a class of that name.
|
||||
|
||||
Contract constraints solve this problem by allowing a contract developer to constrain which ``verify`` functions out of
|
||||
the universe of implementations can be used (i.e. the universe is everything that matches the signature and contract
|
||||
Contract constraints solve this problem by allowing a state creator to constrain which ``verify`` functions out of
|
||||
the universe of implementations can be used (i.e. the universe is everything that matches the class name and contract
|
||||
constraints restrict this universe to a subset). Constraints are satisfied by attachments (JARs). You are not allowed to
|
||||
attach two JARs that both define the same application due to the *no overlap rule*. This rule specifies that two
|
||||
attachment JARs may not provide the same file path. If they do, the transaction is considered invalid. Because each
|
||||
state specifies both a constraint over attachments *and* a Contract class name to use, the specified class must appear
|
||||
in only one attachment.
|
||||
|
||||
So who picks the attachment to use? It is chosen by the creator of the transaction that has to satisfy input constraints.
|
||||
The transaction creator also gets to pick the constraints used by any output states, but the contract logic itself may
|
||||
have opinions about what those constraints are - a typical contract would require that the constraints are propagated,
|
||||
that is, the contract will not just enforce the validity of the next transaction that uses a state, but *all successive
|
||||
transactions as well*. The constraints mechanism creates a balance of power between the creator of data on
|
||||
the ledger and the user who is trying to edit it, which can be very useful when managing upgrades to Corda applications.
|
||||
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).
|
||||
This way, we have both valid data and valid code that checks the transition packed into the transaction.
|
||||
|
||||
So who picks the attachment to use? It is chosen by the creator of the transaction but has to satisfy the constraints of the input states.
|
||||
This is because any node doing transaction resolution will actually verify the selected attachment against all constraints,
|
||||
so the transaction will only be valid if it passes those checks.
|
||||
For example, when the input state is constrained by the ``HashAttachmentConstraint``, can only attach the JAR with that hash to the transaction.
|
||||
|
||||
The transaction creator also gets to pick the constraints used by any output states.
|
||||
When building a transaction, the default constraint on output states is ``AutomaticPlaceholderConstraint``, which means that corda will select the appropriate constraint.
|
||||
Unless specified otherwise, attachment constraints will propagate from input to output states. (The rules are described below)
|
||||
Constraint propagation is also enforced during transaction verification, where for normal transactions (not explicit upgrades, or notary changes),
|
||||
the constraints of the output states are required to "inherit" the constraint of the input states. ( See below for details)
|
||||
|
||||
There are two ways of handling upgrades to a smart contract in Corda:
|
||||
|
||||
@ -86,18 +95,19 @@ time effectively stop being a part of the network.
|
||||
signed by a specified identity, via the regular Java ``jarsigner`` tool. This will be the most flexible type
|
||||
and the smoothest to deploy: no restarts or contract upgrade transactions are needed.
|
||||
|
||||
**Defaults.** 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.
|
||||
**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 (``AutomaticHashConstraint``) is used. This default will be
|
||||
specifying the constraint parameter, a default value (``AutomaticPlaceholderConstraint``) is used. This default will be
|
||||
automatically resolved to a specific ``HashAttachmentConstraint`` or a ``WhitelistedByZoneAttachmentConstraint``.
|
||||
This automatic resolution occurs when a ``TransactionBuilder`` is converted to a ``WireTransaction``. This reduces
|
||||
the boilerplate that would otherwise be involved.
|
||||
|
||||
Finally, an ``AlwaysAcceptAttachmentConstraint`` can be used which accepts anything, though this is intended for
|
||||
testing only.
|
||||
testing only, and a warning will be shown if used by a contract.
|
||||
|
||||
Please note that the ``AttachmentConstraint`` interface is marked as ``@DoNotImplement``. You are not allowed to write
|
||||
new constraint types. Only the platform may implement this interface. If you tried, other nodes would not understand
|
||||
@ -118,48 +128,38 @@ a flow:
|
||||
TransactionBuilder tx = new TransactionBuilder();
|
||||
|
||||
Party notaryParty = ... // a notary party
|
||||
|
||||
tx.addInputState(...)
|
||||
tx.addInputState(...)
|
||||
|
||||
DummyState contractState = new DummyState();
|
||||
|
||||
SecureHash myAttachmentHash = SecureHash.parse("2b4042aed7e0e39d312c4c477dca1d96ec5a878ddcfd5583251a8367edbd4a5f");
|
||||
TransactionState transactionState = new TransactionState(contractState, DummyContract.Companion.getPROGRAMID(), notaryParty, new AttachmentHashConstraint(myAttachmentHash));
|
||||
|
||||
TransactionState transactionState = new TransactionState(contractState, DummyContract.Companion.getPROGRAMID(), notaryParty, null, HashAttachmentConstraint(myhash));
|
||||
tx.addOutputState(transactionState);
|
||||
WireTransaction wtx = tx.toWireTransaction(serviceHub); // This is where an automatic constraint would be resolved.
|
||||
LedgerTransaction ltx = wtx.toLedgerTransaction(serviceHub);
|
||||
ltx.verify(); // Verifies both the attachment constraints and contracts
|
||||
|
||||
Hard-coding the hash of your app in the code itself can be pretty awkward, so the API also offers the ``AutomaticHashConstraint``.
|
||||
This isn't a real constraint that will appear in a transaction: it acts as a marker to the ``TransactionBuilder`` that
|
||||
you require the hash of the node's installed app which supplies the specified contract to be used. In practice, when using
|
||||
hash constraints, you almost always want "whatever the current code is" and not a hard-coded hash. So this automatic
|
||||
constraint placeholder is useful.
|
||||
|
||||
FinalityFlow
|
||||
------------
|
||||
Issues when using the HashAttachmentConstraint
|
||||
----------------------------------------------
|
||||
|
||||
It's possible to encounter contract constraint issues when notarising transactions with the ``FinalityFlow`` on a network
|
||||
containing multiple versions of the same CorDapp. This will happen when using hash constraints or with zone constraints
|
||||
if the zone whitelist has missing CorDapp versions. If a participating party fails to validate the **notarised** transaction
|
||||
then we have a scenario where the members of the network do not have a consistent view of the ledger.
|
||||
When setting up a new network, it is possible to encounter errors when states are issued with the ``HashAttachmentConstraint``,
|
||||
but not all nodes have that same version of the CorDapp installed locally.
|
||||
|
||||
Therefore, if the finality handler flow (which is run on the counter-party) errors for any reason it will always be sent to
|
||||
the flow hospital. From there it's suspended waiting to be retried on node restart. This gives the node operator the opportunity
|
||||
to recover from those errors, which in the case of contract constraint violations means either updating the CorDapp or
|
||||
adding its hash to the zone whitelist.
|
||||
In this case, flows will fail with a ``ContractConstraintRejection``, and the failed flow will be sent to the flow hospital.
|
||||
From there it's suspended waiting to be retried on node restart.
|
||||
This gives the node operator the opportunity to recover from those errors, which in the case of constraint violations means
|
||||
adding the right cordapp jar to the ``cordapps`` folder.
|
||||
|
||||
.. note:: This is a temporary issue in the current version of Corda, until we implement some missing features which will
|
||||
enable a seamless handling of differences in CorDapp versions.
|
||||
|
||||
CorDapps as attachments
|
||||
-----------------------
|
||||
|
||||
CorDapp JARs (see :doc:`cordapp-overview`) that are installed to the node and contain classes implementing the ``Contract``
|
||||
interface are automatically loaded into the ``AttachmentStorage`` of a node at startup.
|
||||
|
||||
After CorDapps are loaded into the attachment store the node creates a link between contract classes and the attachment
|
||||
that they were loaded from. This makes it possible to find the attachment for any given contract. This is how the
|
||||
automatic resolution of attachments is done by the ``TransactionBuilder`` and how, when verifying the constraints and
|
||||
contracts, attachments are associated with their respective contracts.
|
||||
CorDapp JARs (see :doc:`cordapp-overview`) that contain classes implementing the ``Contract`` interface are automatically
|
||||
loaded into the ``AttachmentStorage`` of a node, and made available as ``ContractAttachments``.
|
||||
They are retrievable by hash using ``AttachmentStorage.openAttachment``.
|
||||
These JARs can either be installed on the node or fetched from the network using the ``FetchAttachmentsFlow``.
|
||||
|
||||
.. note:: The obvious way to write a CorDapp is to put all you states, contracts, flows and support code into a single
|
||||
Java module. This will work but it will effectively publish your entire app onto the ledger. That has two problems:
|
||||
@ -168,6 +168,51 @@ contracts, attachments are associated with their respective contracts.
|
||||
app into multiple modules: one which contains just states, contracts and core data types. And another which contains
|
||||
the rest of the app. See :ref:`cordapp-structure`.
|
||||
|
||||
|
||||
Constraints propagation
|
||||
-----------------------
|
||||
|
||||
As was mentioned above, the TransactionBuilder API gives the CorDapp developer or even malicious node owner the possibility
|
||||
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.
|
||||
|
||||
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
|
||||
corresponding input state was the ``HashAttachmentConstraint``. Translated, this means that if this rule is enforced, it ensures
|
||||
that the output state will be spent under similar conditions as it was created.
|
||||
|
||||
Before version 4, the constraint propagation logic was expected to be enforced in the contract verify code, as it has access to the entire Transaction.
|
||||
|
||||
Starting with version 4 of Corda, the constraint propagation logic has been implemented and enforced directly by the platform,
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
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) .
|
||||
|
||||
|
||||
Testing
|
||||
-------
|
||||
|
||||
|
146
docs/source/design/maximus/design.md
Normal file
146
docs/source/design/maximus/design.md
Normal file
@ -0,0 +1,146 @@
|
||||
# Validation of Maximus Scope and Future Work Proposal
|
||||
|
||||
## Introduction
|
||||
|
||||
The intent of this document is to ensure that the Tech Leads and Product Management are comfortable with the proposed
|
||||
direction of HA team future work. The term Maximus has been used widely across R3 and we wish to ensure that the scope
|
||||
is clearly understood and in alignment with wider delivery expectations.
|
||||
|
||||
I hope to explain the successes and failures of our rapid POC work, so it is clearer what guides our decision making in
|
||||
this.
|
||||
|
||||
Also, it will hopefully inform other teams of changes that may cross into their area.
|
||||
|
||||
## What is Maximus?
|
||||
|
||||
Mike’s original proposal for Maximus, made at CordaCon Tokyo 2018, was to use some automation to start and stop node
|
||||
VM’s using some sort of automation to reduce runtime cost. In Mike’s words this would allow ‘huge numbers of
|
||||
identities’, perhaps ‘thousands’.
|
||||
|
||||
The HA team and Andrey Brozhko have tried to stay close to this original definition that Maximus is for managing
|
||||
100’s-1000’s Enterprise Nodes and that the goal of the project is to better manage costs, especially in cloud
|
||||
deployments and with low overall flow rates. However, this leads to the following assumptions:
|
||||
|
||||
1. The overall rate of flows is low and users will accept some latency. The additional sharing of identities on a
|
||||
reduced physical footprint will inevitably reduce throughput compared to dedicated nodes, but should not be a problem.
|
||||
|
||||
2. At least in the earlier phases it is acceptable to statically manage identity keys/certificates for each individual
|
||||
identity. This will be scripted but will incur some effort/procedures/checking on the doorman side.
|
||||
|
||||
3. Every identity has an associated ‘DB schema’, which might be on a shared database server, but the separation is
|
||||
managed at that level. This database is a fixed runtime cost per identity and will not be shared in the earlier phases
|
||||
of Maximus. It might be optionally shareable in future, but this is not a hard requirement for Corda 5 as it needs
|
||||
significant help from core to change the DB schemas. Also, our understanding is that the isolation is a positive feature
|
||||
in some deployments.
|
||||
|
||||
4. Maximus may share infrastructure and possibly JVM memory between identities without breaking some customer
|
||||
requirement for isolation. In other words we are virtualizing the ‘node’, but CorDapps and peer nodes will be unaware of
|
||||
any changes.
|
||||
|
||||
## What Maximus is not
|
||||
|
||||
1. Maximus is not designed to handle millions of identities. That is firmly Marco Polo and possibly handled completely
|
||||
differently.
|
||||
|
||||
2. Maximus should be not priced such as to undercut our own high-performance Enterprise nodes, or allow customers to run
|
||||
arbitrary numbers of nodes for free.
|
||||
|
||||
3. Maximus is not a ‘wallet’ based solution. The nodes in Maximus are fully equivalent to the current Enterprise
|
||||
offering and have first class identities. There is also no remoting of the signing operations.
|
||||
|
||||
## The POC technologies we have tried
|
||||
|
||||
The HA team has looked at several elements of the solution. Some approaches look promising, some do not.
|
||||
|
||||
1. We have already started the work to share a common P2P Artemis between multiple nodes and common bridge/float. This
|
||||
is the ‘SNI header’ work which has been DRB’s recently. This should be functionally complete soon and available in Corda
|
||||
4.0 This work will reduce platform cost and simplify deployment of multiple nodes. For Maximus the main effect is that it
|
||||
should make the configuration much more consistent between nodes and it means that where a node runs is immaterial as
|
||||
the shared broker distributes messages and the Corda firewall handles the public communication.
|
||||
|
||||
2. I looked at flattening the flow state machine, so that we could map Corda operations into combining state and
|
||||
messages in the style of a Map-Reduce pattern. Unfortunately, the work involved is extreme and not compatible with the
|
||||
Corda API. Therefore a pure ‘flow worker’ approach does not look viable any time soon and in general full hot-hot is
|
||||
still a way off.
|
||||
|
||||
3. Chris looked at reducing the essential service set in the node to those needed to support the public flow API and the
|
||||
StateMachine. Then we attached a simple start flow messaging interface. This simple ‘FlowRunner’ class allowed
|
||||
exploration of several options in a gaffer taped state.
|
||||
|
||||
1. We created a simple messaging interface between an RPC runner and a Flow Runner and showed that we can run
|
||||
standard flows.
|
||||
|
||||
2. We were able to POC combining two identities running side-by-side in a Flow Runner, which is in fact quite similar
|
||||
to many of our integration tests. We must address static variable leakage but should be feasible.
|
||||
|
||||
3. We were able to create an RPC worker that could handle several identities at once and start flows on the
|
||||
same/different flow runner harnesses.
|
||||
|
||||
4. We then pushed forward looking into flow sharding. Here we made some progress, but the task started to get more and more
|
||||
complicated. It also highlighted that we don’t have suitable headers on our messages and that the message header
|
||||
whitelist will make this difficult to change whilst maintaining wire compatibility. The conclusion from this is that
|
||||
hot-hot flow sharding will have to wait.
|
||||
|
||||
8. We have been looking at resource/cost management technologies. The almost immediate conclusion is that whilst cloud
|
||||
providers do have automated VM/container as service they are not standardized. Instead, the only standardized approach
|
||||
is Kubernetes+docker, which will charge dynamically according to active use levels.
|
||||
|
||||
9. Looking at resource management in Kubernetes we can dynamically scale relatively homogeneous pods, but the metrics
|
||||
approach cannot easily cope with identity injection. Instead we can scale the number of running pods, but they will have
|
||||
to self-organize the work balancing amongst themselves.
|
||||
|
||||
## Maximus Work Proposal
|
||||
|
||||
#### Current State
|
||||
|
||||

|
||||
|
||||
The current enterprise node solution in GA 3.1 is as above. This has dynamic HA failover available for the bridge/float
|
||||
using ZooKeeper as leader elector, but the node has to be hot-cold. There is some sharing support for the ZooKeeper
|
||||
cluster, but otherwise all this infrastructure has to be replicated per identity. In addition, all elements of this have
|
||||
to have at least one resident instance to ensure that messages are captured and RPC clients have an endpoint to talk to.
|
||||
|
||||
#### Corda 4.0 Agreed Target with SNI Shared Corda Firewalls
|
||||
|
||||

|
||||
|
||||
Here by sharing the P2P Artemis externally and work on the messaging protocol it should be possible to reuse the corda
|
||||
firewall for multiple nodes. This means that the externally advertised address will be stable for the whole cluster
|
||||
independent of the deployed identities. Also, the durable messaging is outside nodes, which means that we can
|
||||
theoretically schedule running the nodes only if a few times a day if they only act in response to external peer
|
||||
messages. Mostly this is a prelude to greater sharing in the future Maximus state.
|
||||
|
||||
#### Intermediate State Explored during POC
|
||||
|
||||

|
||||
|
||||
During the POC we explore the model above, although none of the components were completed to a production standard. The
|
||||
key feature here is that the RPC side has been split out of the node and has API support for multiple identities built
|
||||
in. The flow and P2P elements of the node have been split out too, which means that the ‘FlowWorker’ start-up code can
|
||||
be simpler than the current AbstractNode as it doesn’t have to support the same testing framework. The actual service
|
||||
implementations are unchanged in this.
|
||||
|
||||
The principal communication between the RPC and FlowWorker is about starting flows and completed work is broadcast as
|
||||
events. A message protocol will be defined to allow re-attachment and status querying if the RPC client is restarted.
|
||||
The vault RPC api will continue to the database directly in the RpcWorker and not involve the FlowWorker. The scheduler
|
||||
service will live in the RPC service as potentially the FlowWorkers will not yet be running when the due time occurs.
|
||||
|
||||
#### Proposed Maximus Phase 1 State
|
||||
|
||||

|
||||
|
||||
The productionised version of the above POC will introduce ‘Max Nodes’ that can load FlowWorkers on demand. We still
|
||||
require only one runs at once, but for this we will use ZooKeeper to ensure that FlowWorkers with capacity compete to
|
||||
process the work and only one wins. Based on trials we can safely run a couple of identities at one inside the same Max
|
||||
Node assuming load is manageable. Idle identities will be dropped trivially, since the Hibernate, Artemis connections
|
||||
and thread pools will be owned by the Max Node not the flow workers. At this stage there is no dynamic management of the
|
||||
physical resources, but some sort of scheduler could control how many Max Nodes are running at once.
|
||||
|
||||
#### Final State Maximus with Dynamic Resource Management
|
||||
|
||||

|
||||
|
||||
The final evolution is to add dynamic cost control to the system. As the Max Nodes are homogeneous the RpcWorker can
|
||||
monitor the load and signal metrics available to Kubernetes. This means that Max Nodes can be added and removed as
|
||||
required and potentially cost zero. Ideally, separate work would begin in parallel to combine database data into a
|
||||
single schema, but that is possibly not required.
|
BIN
docs/source/design/maximus/images/current_state.png
Normal file
BIN
docs/source/design/maximus/images/current_state.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 48 KiB |
BIN
docs/source/design/maximus/images/maximus_final.png
Normal file
BIN
docs/source/design/maximus/images/maximus_final.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 99 KiB |
BIN
docs/source/design/maximus/images/maximus_phase1.png
Normal file
BIN
docs/source/design/maximus/images/maximus_phase1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 86 KiB |
BIN
docs/source/design/maximus/images/maximus_poc.png
Normal file
BIN
docs/source/design/maximus/images/maximus_poc.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 72 KiB |
BIN
docs/source/design/maximus/images/shared_bridge_float.png
Normal file
BIN
docs/source/design/maximus/images/shared_bridge_float.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 54 KiB |
@ -125,7 +125,14 @@ The current set of network parameters:
|
||||
:eventHorizon: Time after which nodes are considered to be unresponsive and removed from network map. Nodes republish their
|
||||
``NodeInfo`` on a regular interval. Network map treats that as a heartbeat from the node.
|
||||
|
||||
More parameters will be added in future releases to regulate things like allowed port numbers, whether or not IPv6
|
||||
:packageOwnership: List of the network-wide java packages that were successfully claimed by their owners.
|
||||
Any CorDapp JAR that offers contracts and states in any of these packages must be signed by the owner.
|
||||
This ensures that when a node encounters an owned contract it can uniquely identify it and knows that all other nodes can do the same.
|
||||
Encountering an owned contract in a JAR that is not signed by the rightful owner is most likely a sign of malicious behaviour, and should be reported.
|
||||
The transaction verification logic will throw an exception when this happens.
|
||||
Read more about *Package ownership* here :doc:`design/data-model-upgrades/package-namespace-ownership`.
|
||||
|
||||
More parameters will be added in future releases to regulate things like allowed port numbers, whether or not IPv6
|
||||
connectivity is required for zone members, required cryptographic algorithms and roll-out schedules (e.g. for moving to post quantum cryptography), parameters related to SGX and so on.
|
||||
|
||||
Network parameters update process
|
||||
|
@ -91,6 +91,6 @@ class RaftNotaryServiceTests : IntegrationTest() {
|
||||
val builder = DummyContract.generateInitial(Random().nextInt(), notary, nodeHandle.services.myInfo.singleIdentity().ref(0))
|
||||
val stx = nodeHandle.services.signInitialTransaction(builder)
|
||||
nodeHandle.services.recordTransactions(stx)
|
||||
return StateAndRef(builder.outputStates().first(), StateRef(stx.id, 0))
|
||||
return StateAndRef(stx.coreTransaction.outputs.first(), StateRef(stx.id, 0))
|
||||
}
|
||||
}
|
||||
|
@ -501,7 +501,7 @@ class CashTests {
|
||||
|
||||
private fun makeCash(amount: Amount<Currency>, issuer: AbstractParty, depositRef: Byte = 1) =
|
||||
StateAndRef(
|
||||
TransactionState(Cash.State(amount `issued by` issuer.ref(depositRef), ourIdentity), Cash.PROGRAM_ID, dummyNotary.party),
|
||||
TransactionState(Cash.State(amount `issued by` issuer.ref(depositRef), ourIdentity), Cash.PROGRAM_ID, dummyNotary.party, constraint = AlwaysAcceptAttachmentConstraint),
|
||||
StateRef(SecureHash.randomSHA256(), Random().nextInt(32))
|
||||
)
|
||||
|
||||
|
@ -166,11 +166,11 @@ class ObligationTests {
|
||||
transaction {
|
||||
attachments(Obligation.PROGRAM_ID)
|
||||
output(Obligation.PROGRAM_ID,
|
||||
Obligation.State(
|
||||
obligor = MINI_CORP,
|
||||
quantity = 1000.DOLLARS.quantity,
|
||||
beneficiary = CHARLIE,
|
||||
template = megaCorpDollarSettlement))
|
||||
Obligation.State(
|
||||
obligor = MINI_CORP,
|
||||
quantity = 1000.DOLLARS.quantity,
|
||||
beneficiary = CHARLIE,
|
||||
template = megaCorpDollarSettlement))
|
||||
command(MINI_CORP_PUBKEY, Obligation.Commands.Issue())
|
||||
this.verifies()
|
||||
}
|
||||
@ -314,7 +314,7 @@ class ObligationTests {
|
||||
}
|
||||
|
||||
private inline fun <reified T : ContractState> getStateAndRef(state: T, contractClassName: ContractClassName): StateAndRef<T> {
|
||||
val txState = TransactionState(state, contractClassName, DUMMY_NOTARY)
|
||||
val txState = TransactionState(state, contractClassName, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint)
|
||||
return StateAndRef(txState, StateRef(SecureHash.randomSHA256(), 0))
|
||||
|
||||
}
|
||||
@ -701,10 +701,10 @@ class ObligationTests {
|
||||
attachments(Obligation.PROGRAM_ID)
|
||||
input(Obligation.PROGRAM_ID, inState)
|
||||
input(Obligation.PROGRAM_ID,
|
||||
inState.copy(
|
||||
quantity = 15000,
|
||||
template = megaCorpPoundSettlement,
|
||||
beneficiary = AnonymousParty(BOB_PUBKEY)))
|
||||
inState.copy(
|
||||
quantity = 15000,
|
||||
template = megaCorpPoundSettlement,
|
||||
beneficiary = AnonymousParty(BOB_PUBKEY)))
|
||||
output(Obligation.PROGRAM_ID, outState.copy(quantity = 115000))
|
||||
command(MINI_CORP_PUBKEY, Obligation.Commands.Move())
|
||||
this `fails with` "the amounts balance"
|
||||
@ -962,4 +962,4 @@ class ObligationTests {
|
||||
get() = Obligation.Terms(NonEmptySet.of(cashContractBytes.sha256() as SecureHash), NonEmptySet.of(this), TEST_TX_TIME)
|
||||
private val Amount<Issued<Currency>>.OBLIGATION: Obligation.State<Currency>
|
||||
get() = Obligation.State(Obligation.Lifecycle.NORMAL, DUMMY_OBLIGATION_ISSUER, token.OBLIGATION_DEF, quantity, NULL_PARTY)
|
||||
}
|
||||
}
|
@ -1,14 +1,8 @@
|
||||
package net.corda.nodeapi.internal.persistence
|
||||
|
||||
import org.hibernate.stat.*
|
||||
import javax.management.MXBean
|
||||
|
||||
import org.hibernate.stat.Statistics
|
||||
import org.hibernate.stat.SecondLevelCacheStatistics
|
||||
import org.hibernate.stat.QueryStatistics
|
||||
import org.hibernate.stat.NaturalIdCacheStatistics
|
||||
import org.hibernate.stat.EntityStatistics
|
||||
import org.hibernate.stat.CollectionStatistics
|
||||
|
||||
/**
|
||||
* Exposes Hibernate [Statistics] contract as JMX resource.
|
||||
*/
|
||||
@ -20,6 +14,25 @@ interface StatisticsService : Statistics
|
||||
* session factory.
|
||||
*/
|
||||
class DelegatingStatisticsService(private val delegate: Statistics) : StatisticsService {
|
||||
override fun getNaturalIdStatistics(entityName: String?): NaturalIdStatistics {
|
||||
return delegate.getNaturalIdStatistics(entityName)
|
||||
}
|
||||
|
||||
override fun getDomainDataRegionStatistics(regionName: String?): CacheRegionStatistics {
|
||||
return delegate.getDomainDataRegionStatistics(regionName)
|
||||
}
|
||||
|
||||
override fun getQueryRegionStatistics(regionName: String?): CacheRegionStatistics {
|
||||
return delegate.getQueryRegionStatistics(regionName)
|
||||
}
|
||||
|
||||
override fun getNaturalIdQueryExecutionMaxTimeEntity(): String {
|
||||
return delegate.getNaturalIdQueryExecutionMaxTimeEntity()
|
||||
}
|
||||
|
||||
override fun getCacheRegionStatistics(regionName: String?): CacheRegionStatistics {
|
||||
return delegate.getCacheRegionStatistics(regionName)
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
delegate.clear()
|
||||
|
@ -44,7 +44,7 @@ class AttachmentsClassLoaderStaticContractTests {
|
||||
|
||||
class AttachmentDummyContract : Contract {
|
||||
companion object {
|
||||
private const val ATTACHMENT_PROGRAM_ID = "net.corda.nodeapi.internal.AttachmentsClassLoaderStaticContractTests\$AttachmentDummyContract"
|
||||
const val ATTACHMENT_PROGRAM_ID = "net.corda.nodeapi.internal.AttachmentsClassLoaderStaticContractTests\$AttachmentDummyContract"
|
||||
}
|
||||
|
||||
data class State(val magicNumber: Int = 0) : ContractState {
|
||||
@ -80,7 +80,14 @@ class AttachmentsClassLoaderStaticContractTests {
|
||||
cordappProviderImpl.start(testNetworkParameters().whitelistedContractImplementations)
|
||||
doReturn(cordappProviderImpl).whenever(it).cordappProvider
|
||||
doReturn(testNetworkParameters()).whenever(it).networkParameters
|
||||
doReturn(attachments).whenever(it).attachments
|
||||
val attachmentStorage = rigorousMock<AttachmentStorage>()
|
||||
doReturn(attachmentStorage).whenever(it).attachments
|
||||
val attachment = rigorousMock<ContractAttachment>()
|
||||
doReturn(attachment).whenever(attachmentStorage).openAttachment(any())
|
||||
doReturn(it.cordappProvider.getContractAttachmentID(AttachmentDummyContract.ATTACHMENT_PROGRAM_ID)).whenever(attachment).id
|
||||
doReturn(setOf(AttachmentDummyContract.ATTACHMENT_PROGRAM_ID)).whenever(attachment).allContracts
|
||||
doReturn("app").whenever(attachment).uploader
|
||||
doReturn(emptyList<Party>()).whenever(attachment).signers
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -172,7 +172,9 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(), attachments).tokenize()
|
||||
@Suppress("LeakingThis")
|
||||
val keyManagementService = makeKeyManagementService(identityService).tokenize()
|
||||
val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, transactionStorage)
|
||||
val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, transactionStorage).also {
|
||||
attachments.servicesForResolution = it
|
||||
}
|
||||
@Suppress("LeakingThis")
|
||||
val vaultService = makeVaultService(keyManagementService, servicesForResolution, database).tokenize()
|
||||
val nodeProperties = NodePropertiesPersistentStore(StubbedNodeUniqueIdProvider::value, database, cacheFactory)
|
||||
@ -1072,7 +1074,7 @@ fun createCordaPersistence(databaseConfig: DatabaseConfig,
|
||||
// so we end up providing both descriptor and converter. We should re-examine this in later versions to see if
|
||||
// either Hibernate can be convinced to stop warning, use the descriptor by default, or something else.
|
||||
JavaTypeDescriptorRegistry.INSTANCE.addDescriptor(AbstractPartyDescriptor(wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous))
|
||||
val attributeConverters = listOf(AbstractPartyToX500NameAsStringConverter(wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous))
|
||||
val attributeConverters = listOf(PublicKeyToTextConverter(), AbstractPartyToX500NameAsStringConverter(wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous))
|
||||
return CordaPersistence(databaseConfig, schemaService.schemaOptions.keys, cacheFactory, attributeConverters)
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@ package net.corda.node.internal.cordapp
|
||||
|
||||
import io.github.classgraph.ClassGraph
|
||||
import io.github.classgraph.ScanResult
|
||||
import net.corda.core.contracts.warnContractWithoutConstraintPropagation
|
||||
import net.corda.core.cordapp.Cordapp
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.sha256
|
||||
@ -184,7 +185,11 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
|
||||
}
|
||||
|
||||
private fun findContractClassNames(scanResult: RestrictedScanResult): List<String> {
|
||||
return coreContractClasses.flatMap { scanResult.getNamesOfClassesImplementing(it) }.distinct()
|
||||
val contractClasses = coreContractClasses.flatMap { scanResult.getNamesOfClassesImplementing(it) }.distinct()
|
||||
for (contractClass in contractClasses) {
|
||||
contractClass.warnContractWithoutConstraintPropagation(appClassLoader)
|
||||
}
|
||||
return contractClasses
|
||||
}
|
||||
|
||||
private fun findPlugins(cordappJarPath: RestrictedURL): List<SerializationWhitelist> {
|
||||
|
@ -209,15 +209,17 @@ object DefaultKryoCustomizer {
|
||||
output.writeString(obj.contract)
|
||||
kryo.writeClassAndObject(output, obj.additionalContracts)
|
||||
output.writeString(obj.uploader)
|
||||
kryo.writeClassAndObject(output, obj.signers)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<ContractAttachment>): ContractAttachment {
|
||||
if (kryo.serializationContext() != null) {
|
||||
val attachmentHash = SecureHash.SHA256(input.readBytes(32))
|
||||
val contract = input.readString()
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val additionalContracts = kryo.readClassAndObject(input) as Set<ContractClassName>
|
||||
val uploader = input.readString()
|
||||
val signers = kryo.readClassAndObject(input) as List<PublicKey>
|
||||
val context = kryo.serializationContext()!!
|
||||
val attachmentStorage = context.serviceHub.attachments
|
||||
|
||||
@ -229,14 +231,14 @@ object DefaultKryoCustomizer {
|
||||
override val id = attachmentHash
|
||||
}
|
||||
|
||||
return ContractAttachment(lazyAttachment, contract, additionalContracts, uploader)
|
||||
return ContractAttachment(lazyAttachment, contract, additionalContracts, uploader, signers)
|
||||
} else {
|
||||
val attachment = GeneratedAttachment(input.readBytesWithLength())
|
||||
val contract = input.readString()
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val additionalContracts = kryo.readClassAndObject(input) as Set<ContractClassName>
|
||||
val uploader = input.readString()
|
||||
return ContractAttachment(attachment, contract, additionalContracts, uploader)
|
||||
val signers = kryo.readClassAndObject(input) as List<PublicKey>
|
||||
return ContractAttachment(attachment, contract, additionalContracts, uploader, signers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,13 +6,16 @@ import com.google.common.hash.HashCode
|
||||
import com.google.common.hash.Hashing
|
||||
import com.google.common.hash.HashingInputStream
|
||||
import com.google.common.io.CountingInputStream
|
||||
import net.corda.core.ClientRelevantError
|
||||
import net.corda.core.CordaRuntimeException
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.contracts.ContractAttachment
|
||||
import net.corda.core.contracts.ContractClassName
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
import net.corda.core.node.services.vault.AttachmentQueryCriteria
|
||||
import net.corda.core.node.services.vault.AttachmentSort
|
||||
@ -30,6 +33,7 @@ import java.io.FilterInputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.nio.file.Paths
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import java.util.jar.JarInputStream
|
||||
@ -45,6 +49,10 @@ class NodeAttachmentService(
|
||||
cacheFactory: NamedCacheFactory,
|
||||
private val database: CordaPersistence
|
||||
) : AttachmentStorageInternal, SingletonSerializeAsToken() {
|
||||
|
||||
// This is to break the circular dependency.
|
||||
lateinit var servicesForResolution: ServicesForResolution
|
||||
|
||||
companion object {
|
||||
private val log = contextLogger()
|
||||
|
||||
@ -94,7 +102,13 @@ class NodeAttachmentService(
|
||||
@Column(name = "contract_class_name", nullable = false)
|
||||
@CollectionTable(name = "${NODE_DATABASE_PREFIX}attachments_contracts", joinColumns = [(JoinColumn(name = "att_id", referencedColumnName = "att_id"))],
|
||||
foreignKey = ForeignKey(name = "FK__ctr_class__attachments"))
|
||||
var contractClassNames: List<ContractClassName>? = null
|
||||
var contractClassNames: List<ContractClassName>? = null,
|
||||
|
||||
@ElementCollection(targetClass = PublicKey::class, fetch = FetchType.EAGER)
|
||||
@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
|
||||
)
|
||||
|
||||
@VisibleForTesting
|
||||
@ -212,11 +226,13 @@ class NodeAttachmentService(
|
||||
|
||||
private fun loadAttachmentContent(id: SecureHash): Pair<Attachment, ByteArray>? {
|
||||
return database.transaction {
|
||||
val attachment = currentDBSession().get(NodeAttachmentService.DBAttachment::class.java, id.toString()) ?: return@transaction null
|
||||
val attachment = currentDBSession().get(NodeAttachmentService.DBAttachment::class.java, id.toString())
|
||||
?: return@transaction null
|
||||
val attachmentImpl = AttachmentImpl(id, { attachment.content }, checkAttachmentsOnLoad).let {
|
||||
val contracts = attachment.contractClassNames
|
||||
if (contracts != null && contracts.isNotEmpty()) {
|
||||
ContractAttachment(it, contracts.first(), contracts.drop(1).toSet(), attachment.uploader)
|
||||
ContractAttachment(it, contracts.first(), contracts.drop(1).toSet(), attachment.uploader, attachment.signers
|
||||
?: emptyList())
|
||||
} else {
|
||||
it
|
||||
}
|
||||
@ -290,14 +306,19 @@ class NodeAttachmentService(
|
||||
val id = bytes.sha256()
|
||||
if (!hasAttachment(id)) {
|
||||
checkIsAValidJAR(bytes.inputStream())
|
||||
|
||||
val jarSigners = getSigners(bytes)
|
||||
|
||||
val session = currentDBSession()
|
||||
val attachment = NodeAttachmentService.DBAttachment(
|
||||
attId = id.toString(),
|
||||
content = bytes,
|
||||
uploader = uploader,
|
||||
filename = filename,
|
||||
contractClassNames = contractClassNames
|
||||
contractClassNames = contractClassNames,
|
||||
signers = jarSigners
|
||||
)
|
||||
|
||||
session.save(attachment)
|
||||
attachmentCount.inc()
|
||||
log.info("Stored new attachment $id")
|
||||
@ -309,6 +330,9 @@ class NodeAttachmentService(
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSigners(attachmentBytes: ByteArray) =
|
||||
JarSignatureCollector.collectSigners(JarInputStream(attachmentBytes.inputStream()))
|
||||
|
||||
@Suppress("OverridingDeprecatedMember")
|
||||
override fun importOrGetAttachment(jar: InputStream): AttachmentId {
|
||||
return try {
|
||||
@ -339,4 +363,4 @@ class NodeAttachmentService(
|
||||
query.resultList.map { AttachmentId.parse(it.attId) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package net.corda.node.services.persistence
|
||||
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.utilities.hexToByteArray
|
||||
import net.corda.core.utilities.toHex
|
||||
import java.security.PublicKey
|
||||
import javax.persistence.AttributeConverter
|
||||
import javax.persistence.Converter
|
||||
|
||||
/**
|
||||
* Converts to and from a Public key into a hex encoded string.
|
||||
* Used by JPA to automatically map a [PublicKey] to a text column
|
||||
*/
|
||||
@Converter(autoApply = true)
|
||||
class PublicKeyToTextConverter : AttributeConverter<PublicKey, String> {
|
||||
override fun convertToDatabaseColumn(key: PublicKey?): String? = key?.encoded?.toHex()
|
||||
override fun convertToEntityAttribute(text: String?): PublicKey? = text?.let { Crypto.decodePublicKey(it.hexToByteArray()) }
|
||||
}
|
@ -564,7 +564,8 @@ class NodeVaultService(
|
||||
// Even if we set the default pageNumber to be 1 instead, that may not cover the non-default cases.
|
||||
// So the floor may be necessary anyway.
|
||||
query.firstResult = maxOf(0, (paging.pageNumber - 1) * paging.pageSize)
|
||||
query.maxResults = paging.pageSize + 1 // detection too many results
|
||||
val pageSize = paging.pageSize + 1
|
||||
query.maxResults = if (pageSize > 0) pageSize else Integer.MAX_VALUE // detection too many results, protected against overflow
|
||||
|
||||
// execution
|
||||
val results = query.resultList
|
||||
|
@ -14,4 +14,17 @@
|
||||
<renameTable oldTableName="NODE_ATTACHMENTS_CONTRACT_CLASS_NAME" newTableName="NODE_ATTACHMENTS_CONTRACTS" />
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="R3.Corda" id="add_signers">
|
||||
<createTable tableName="node_attachments_signers">
|
||||
<column name="att_id" type="NVARCHAR(255)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="signer" type="NVARCHAR(1024)"/>
|
||||
</createTable>
|
||||
|
||||
<addForeignKeyConstraint baseColumnNames="att_id" baseTableName="node_attachments_signers"
|
||||
constraintName="FK__signers__attachments"
|
||||
referencedColumnNames="att_id" referencedTableName="node_attachments"/>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
@ -2,6 +2,8 @@ package net.corda.node.internal
|
||||
|
||||
import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.node.JavaPackageName
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.finance.DOLLARS
|
||||
@ -10,6 +12,7 @@ import net.corda.node.services.config.NotaryConfig
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.nodeapi.internal.network.NetworkParametersCopier
|
||||
import net.corda.core.node.NotaryInfo
|
||||
import net.corda.core.utilities.days
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
||||
@ -65,7 +68,8 @@ class NetworkParametersTest {
|
||||
fun `choosing notary not specified in network parameters will fail`() {
|
||||
val fakeNotary = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME, configOverrides = {
|
||||
val notary = NotaryConfig(false)
|
||||
doReturn(notary).whenever(it).notary}))
|
||||
doReturn(notary).whenever(it).notary
|
||||
}))
|
||||
val fakeNotaryId = fakeNotary.info.singleIdentity()
|
||||
val alice = mockNet.createPartyNode(ALICE_NAME)
|
||||
assertThat(alice.services.networkMapCache.notaryIdentities).doesNotContain(fakeNotaryId)
|
||||
@ -87,6 +91,62 @@ class NetworkParametersTest {
|
||||
}.withMessage("maxTransactionSize cannot be bigger than maxMessageSize")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `package ownership checks are correct`() {
|
||||
val key1 = generateKeyPair().public
|
||||
val key2 = generateKeyPair().public
|
||||
|
||||
assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy {
|
||||
NetworkParameters(1,
|
||||
emptyList(),
|
||||
2001,
|
||||
2000,
|
||||
Instant.now(),
|
||||
1,
|
||||
emptyMap(),
|
||||
Int.MAX_VALUE.days,
|
||||
mapOf(
|
||||
JavaPackageName("com.!example.stuff") to key2
|
||||
)
|
||||
)
|
||||
}.withMessageContaining("Attempting to whitelist illegal java package")
|
||||
|
||||
assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy {
|
||||
NetworkParameters(1,
|
||||
emptyList(),
|
||||
2001,
|
||||
2000,
|
||||
Instant.now(),
|
||||
1,
|
||||
emptyMap(),
|
||||
Int.MAX_VALUE.days,
|
||||
mapOf(
|
||||
JavaPackageName("com.example") to key1,
|
||||
JavaPackageName("com.example.stuff") to key2
|
||||
)
|
||||
)
|
||||
}.withMessage("multiple packages added to the packageOwnership overlap.")
|
||||
|
||||
NetworkParameters(1,
|
||||
emptyList(),
|
||||
2001,
|
||||
2000,
|
||||
Instant.now(),
|
||||
1,
|
||||
emptyMap(),
|
||||
Int.MAX_VALUE.days,
|
||||
mapOf(
|
||||
JavaPackageName("com.example") to key1,
|
||||
JavaPackageName("com.examplestuff") to key2
|
||||
)
|
||||
)
|
||||
|
||||
assert(JavaPackageName("com.example").owns("com.example.something.MyClass"))
|
||||
assert(!JavaPackageName("com.example").owns("com.examplesomething.MyClass"))
|
||||
assert(!JavaPackageName("com.exam").owns("com.example.something.MyClass"))
|
||||
|
||||
}
|
||||
|
||||
// Helpers
|
||||
private fun dropParametersToDir(dir: Path, params: NetworkParameters) {
|
||||
NetworkParametersCopier(params).install(dir)
|
||||
|
@ -4,10 +4,16 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import com.codahale.metrics.MetricRegistry
|
||||
import com.google.common.jimfs.Configuration
|
||||
import com.google.common.jimfs.Jimfs
|
||||
import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.JarSignatureTestUtils.createJar
|
||||
import net.corda.core.JarSignatureTestUtils.generateKey
|
||||
import net.corda.core.JarSignatureTestUtils.signJar
|
||||
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.AttachmentSort
|
||||
import net.corda.core.node.services.vault.Builder
|
||||
@ -16,34 +22,42 @@ 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.common.internal.testNetworkParameters
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.internal.LogHelper
|
||||
import net.corda.testing.internal.TestingNamedCacheFactory
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import net.corda.testing.internal.configureDatabase
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
|
||||
import net.corda.testing.node.internal.InternalMockNetwork
|
||||
import net.corda.testing.node.internal.startFlow
|
||||
import org.assertj.core.api.Assertions.assertThatIllegalArgumentException
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import org.junit.*
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.OutputStream
|
||||
import java.net.URI
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.FileAlreadyExistsException
|
||||
import java.nio.file.FileSystem
|
||||
import java.nio.file.Path
|
||||
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 kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertNull
|
||||
|
||||
|
||||
class NodeAttachmentServiceTest {
|
||||
|
||||
// Use an in memory file system for testing attachment storage.
|
||||
private lateinit var fs: FileSystem
|
||||
private lateinit var database: CordaPersistence
|
||||
private lateinit var storage: NodeAttachmentService
|
||||
private val services = rigorousMock<ServicesForResolution>()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
@ -52,18 +66,40 @@ class NodeAttachmentServiceTest {
|
||||
val dataSourceProperties = makeTestDataSourceProperties()
|
||||
database = configureDatabase(dataSourceProperties, DatabaseConfig(runMigration = true), { null }, { null })
|
||||
fs = Jimfs.newFileSystem(Configuration.unix())
|
||||
|
||||
doReturn(testNetworkParameters()).whenever(services).networkParameters
|
||||
|
||||
storage = NodeAttachmentService(MetricRegistry(), TestingNamedCacheFactory(), database).also {
|
||||
database.transaction {
|
||||
it.start()
|
||||
}
|
||||
}
|
||||
storage.servicesForResolution = services
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
dir.list { subdir ->
|
||||
subdir.forEach(Path::deleteRecursively)
|
||||
}
|
||||
database.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `importing a signed jar saves the signers to the storage`() {
|
||||
val jarAndSigner = makeTestSignedContractJar("com.example.MyContract")
|
||||
val signedJar = jarAndSigner.first
|
||||
val attachmentId = storage.importAttachment(signedJar.inputStream(), "test", null)
|
||||
assertEquals(listOf(jarAndSigner.second.hash), storage.openAttachment(attachmentId)!!.signers.map { it.hash })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `importing a non-signed jar will save no signers`() {
|
||||
val jarName = makeTestContractJar("com.example.MyContract")
|
||||
val attachmentId = storage.importAttachment(dir.resolve(jarName).inputStream(), "test", null)
|
||||
assertEquals(0, storage.openAttachment(attachmentId)!!.signers.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `insert and retrieve`() {
|
||||
val (testJar, expectedHash) = makeTestJar()
|
||||
@ -289,7 +325,20 @@ class NodeAttachmentServiceTest {
|
||||
return Pair(file, file.readAll().sha256())
|
||||
}
|
||||
|
||||
private companion object {
|
||||
companion object {
|
||||
private val dir = Files.createTempDirectory(NodeAttachmentServiceTest::class.simpleName)
|
||||
|
||||
@BeforeClass
|
||||
@JvmStatic
|
||||
fun beforeClass() {
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
@JvmStatic
|
||||
fun afterClass() {
|
||||
dir.deleteRecursively()
|
||||
}
|
||||
|
||||
private fun makeTestJar(output: OutputStream, extraEntries: List<Pair<String, String>> = emptyList()) {
|
||||
output.use {
|
||||
val jar = JarOutputStream(it)
|
||||
@ -305,5 +354,48 @@ class NodeAttachmentServiceTest {
|
||||
jar.closeEntry()
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeTestSignedContractJar(contractName: String): Pair<Path, PublicKey> {
|
||||
val alias = "testAlias"
|
||||
val pwd = "testPassword"
|
||||
dir.generateKey(alias, pwd, ALICE_NAME.toString())
|
||||
val jarName = makeTestContractJar(contractName)
|
||||
val signer = dir.signJar(jarName, alias, pwd)
|
||||
return dir.resolve(jarName) to signer
|
||||
}
|
||||
|
||||
private fun makeTestContractJar(contractName: String): String {
|
||||
val packages = contractName.split(".")
|
||||
val jarName = "testattachment.jar"
|
||||
val className = packages.last()
|
||||
createTestClass(className, packages.subList(0, packages.size - 1))
|
||||
dir.createJar(jarName, "${contractName.replace(".", "/")}.class")
|
||||
return jarName
|
||||
}
|
||||
|
||||
private fun createTestClass(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(dir.toFile()))
|
||||
|
||||
val compile = 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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,9 +1,6 @@
|
||||
package net.corda.node.services.schema
|
||||
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TransactionState
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
@ -66,7 +63,7 @@ class PersistentStateServiceTests {
|
||||
val persistentStateService = PersistentStateService(schemaService)
|
||||
database.transaction {
|
||||
val MEGA_CORP = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")).party
|
||||
persistentStateService.persist(setOf(StateAndRef(TransactionState(TestState(), DummyContract.PROGRAM_ID, MEGA_CORP), StateRef(SecureHash.sha256("dummy"), 0))))
|
||||
persistentStateService.persist(setOf(StateAndRef(TransactionState(TestState(), DummyContract.PROGRAM_ID, MEGA_CORP, constraint = AlwaysAcceptAttachmentConstraint), StateRef(SecureHash.sha256("dummy"), 0))))
|
||||
currentDBSession().flush()
|
||||
val parentRowCountResult = connection.prepareStatement("select count(*) from Parents").executeQuery()
|
||||
parentRowCountResult.next()
|
||||
|
@ -332,6 +332,6 @@ class ValidatingNotaryServiceTests {
|
||||
val signedByNode = serviceHub.signInitialTransaction(tx)
|
||||
val stx = notaryNode.services.addSignature(signedByNode, notary.owningKey)
|
||||
serviceHub.recordTransactions(stx)
|
||||
return StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0))
|
||||
return StateAndRef(stx.coreTransaction.outputs.first(), StateRef(stx.id, 0))
|
||||
}
|
||||
}
|
||||
|
@ -481,7 +481,7 @@ abstract class VaultQueryTestsBase : VaultQueryParties {
|
||||
vaultFiller.fillWithSomeTestLinearStates(1, constraint = AlwaysAcceptAttachmentConstraint).states.first().state.constraint
|
||||
vaultFiller.fillWithSomeTestLinearStates(1, constraint = WhitelistedByZoneAttachmentConstraint).states.first().state.constraint
|
||||
// hash constraint
|
||||
val linearStateHash = vaultFiller.fillWithSomeTestLinearStates(1, constraint = HashAttachmentConstraint(SecureHash.randomSHA256()))
|
||||
val linearStateHash = vaultFiller.fillWithSomeTestLinearStates(1, constraint = AutomaticPlaceholderConstraint) // defaults to the HashConstraint
|
||||
val constraintHash = linearStateHash.states.first().state.constraint as HashAttachmentConstraint
|
||||
// signature constraint (single key)
|
||||
val linearStateSignature = vaultFiller.fillWithSomeTestLinearStates(1, constraint = SignatureAttachmentConstraint(alice.publicKey))
|
||||
@ -504,7 +504,7 @@ abstract class VaultQueryTestsBase : VaultQueryParties {
|
||||
val constraintTypeCriteria2 = VaultQueryCriteria(constraintTypes = setOf(HASH))
|
||||
val constraintResults2 = vaultService.queryBy<LinearState>(constraintTypeCriteria2)
|
||||
assertThat(constraintResults2.states).hasSize(2)
|
||||
assertThat(constraintResults2.states.map { it.state.constraint }).containsOnlyOnce(constraintHash)
|
||||
assertThat(constraintResults2.states.map { it.state.constraint }.toSet()).isEqualTo(setOf(constraintHash))
|
||||
|
||||
// search for states with [Vault.ConstraintInfo.Type] either HASH or CZ_WHITELISED
|
||||
// DOCSTART VaultQueryExample30
|
||||
@ -536,7 +536,7 @@ abstract class VaultQueryTestsBase : VaultQueryParties {
|
||||
val alwaysAcceptConstraint = vaultFiller.fillWithSomeTestLinearStates(1, constraint = AlwaysAcceptAttachmentConstraint).states.first().state.constraint
|
||||
vaultFiller.fillWithSomeTestLinearStates(1, constraint = WhitelistedByZoneAttachmentConstraint)
|
||||
// hash constraint
|
||||
val linearStateHash = vaultFiller.fillWithSomeTestLinearStates(1, constraint = HashAttachmentConstraint(SecureHash.randomSHA256()))
|
||||
val linearStateHash = vaultFiller.fillWithSomeTestLinearStates(1, constraint = AutomaticPlaceholderConstraint) // defaults to the hash constraint.
|
||||
val constraintHash = linearStateHash.states.first().state.constraint as HashAttachmentConstraint
|
||||
// signature constraint (single key)
|
||||
val linearStateSignature = vaultFiller.fillWithSomeTestLinearStates(1, constraint = SignatureAttachmentConstraint(alice.publicKey))
|
||||
@ -559,7 +559,7 @@ abstract class VaultQueryTestsBase : VaultQueryParties {
|
||||
// search for states for a specific HashAttachmentConstraint
|
||||
val constraintsCriteria2 = VaultQueryCriteria(constraints = setOf(Vault.ConstraintInfo(constraintHash)))
|
||||
val constraintResults2 = vaultService.queryBy<LinearState>(constraintsCriteria2)
|
||||
assertThat(constraintResults2.states).hasSize(1)
|
||||
assertThat(constraintResults2.states).hasSize(2)
|
||||
assertThat(constraintResults2.states.first().state.constraint).isEqualTo(constraintHash)
|
||||
|
||||
// search for states with a specific SignatureAttachmentConstraint constraint
|
||||
@ -574,7 +574,7 @@ abstract class VaultQueryTestsBase : VaultQueryParties {
|
||||
Vault.ConstraintInfo(constraintSignatureCompositeKey), Vault.ConstraintInfo(constraintHash)))
|
||||
val constraintResults = vaultService.queryBy<LinearState>(constraintCriteria)
|
||||
// DOCEND VaultQueryExample31
|
||||
assertThat(constraintResults.states).hasSize(3)
|
||||
assertThat(constraintResults.states).hasSize(4)
|
||||
assertThat(constraintResults.states.map { it.state.constraint }).containsAll(listOf(constraintHash, constraintSignature, constraintSignatureCompositeKey))
|
||||
|
||||
// exercise enriched query
|
||||
@ -2337,7 +2337,7 @@ abstract class VaultQueryTestsBase : VaultQueryParties {
|
||||
database.transaction {
|
||||
vaultFiller.fillWithSomeTestLinearStates(1, constraint = WhitelistedByZoneAttachmentConstraint)
|
||||
vaultFiller.fillWithSomeTestLinearStates(1, constraint = SignatureAttachmentConstraint(alice.publicKey))
|
||||
vaultFiller.fillWithSomeTestLinearStates(1, constraint = HashAttachmentConstraint( SecureHash.randomSHA256()))
|
||||
vaultFiller.fillWithSomeTestLinearStates(1, constraint = AutomaticPlaceholderConstraint) // this defaults to the HashConstraint
|
||||
vaultFiller.fillWithSomeTestLinearStates(1, constraint = AlwaysAcceptAttachmentConstraint)
|
||||
|
||||
// Base criteria
|
||||
|
@ -241,7 +241,7 @@ class MySQLNotaryServiceTests : IntegrationTest() {
|
||||
val builder = DummyContract.generateInitial(Random().nextInt(), notary, node.info.singleIdentity().ref(0))
|
||||
val stx = node.services.signInitialTransaction(builder)
|
||||
node.services.recordTransactions(stx)
|
||||
StateAndRef(builder.outputStates().first(), StateRef(stx.id, 0))
|
||||
StateAndRef(stx.coreTransaction.outputs.first(), StateRef(stx.id, 0))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -116,7 +116,7 @@ class CommercialPaperTestsGeneric {
|
||||
|
||||
// Some CP is issued onto the ledger by MegaCorp.
|
||||
transaction("Issuance") {
|
||||
attachments(CP_PROGRAM_ID, CommercialPaper.CP_PROGRAM_ID)
|
||||
attachments(CP_PROGRAM_ID)
|
||||
output(thisTest.getContract(), "paper", thisTest.getPaper())
|
||||
command(MEGA_CORP_PUBKEY, thisTest.getIssueCommand(DUMMY_NOTARY))
|
||||
timeWindow(TEST_TX_TIME)
|
||||
@ -126,7 +126,7 @@ class CommercialPaperTestsGeneric {
|
||||
// The CP is sold to alice for her $900, $100 less than the face value. At 10% interest after only 7 days,
|
||||
// that sounds a bit too good to be true!
|
||||
transaction("Trade") {
|
||||
attachments(Cash.PROGRAM_ID, CommercialPaper.CP_PROGRAM_ID)
|
||||
attachments(Cash.PROGRAM_ID)
|
||||
input("paper")
|
||||
input("alice's $900")
|
||||
output(Cash.PROGRAM_ID, "borrowed $900", 900.DOLLARS.CASH issuedBy issuer ownedBy MEGA_CORP)
|
||||
@ -139,7 +139,7 @@ class CommercialPaperTestsGeneric {
|
||||
// Time passes, and Alice redeem's her CP for $1000, netting a $100 profit. MegaCorp has received $1200
|
||||
// as a single payment from somewhere and uses it to pay Alice off, keeping the remaining $200 as change.
|
||||
transaction("Redemption") {
|
||||
attachments(CP_PROGRAM_ID, CommercialPaper.CP_PROGRAM_ID)
|
||||
attachments(CP_PROGRAM_ID)
|
||||
input("alice's paper")
|
||||
input("some profits")
|
||||
|
||||
@ -182,7 +182,6 @@ class CommercialPaperTestsGeneric {
|
||||
@Test
|
||||
fun `key mismatch at issue`() {
|
||||
transaction {
|
||||
attachment(CP_PROGRAM_ID)
|
||||
attachment(CP_PROGRAM_ID)
|
||||
output(thisTest.getContract(), thisTest.getPaper())
|
||||
command(MINI_CORP_PUBKEY, thisTest.getIssueCommand(DUMMY_NOTARY))
|
||||
@ -194,7 +193,6 @@ class CommercialPaperTestsGeneric {
|
||||
@Test
|
||||
fun `face value is not zero`() {
|
||||
transaction {
|
||||
attachment(CP_PROGRAM_ID)
|
||||
attachment(CP_PROGRAM_ID)
|
||||
output(thisTest.getContract(), thisTest.getPaper().withFaceValue(0.DOLLARS `issued by` issuer))
|
||||
command(MEGA_CORP_PUBKEY, thisTest.getIssueCommand(DUMMY_NOTARY))
|
||||
@ -206,7 +204,6 @@ class CommercialPaperTestsGeneric {
|
||||
@Test
|
||||
fun `maturity date not in the past`() {
|
||||
transaction {
|
||||
attachment(CP_PROGRAM_ID)
|
||||
attachment(CP_PROGRAM_ID)
|
||||
output(thisTest.getContract(), thisTest.getPaper().withMaturityDate(TEST_TX_TIME - 10.days))
|
||||
command(MEGA_CORP_PUBKEY, thisTest.getIssueCommand(DUMMY_NOTARY))
|
||||
@ -218,7 +215,6 @@ class CommercialPaperTestsGeneric {
|
||||
@Test
|
||||
fun `issue cannot replace an existing state`() {
|
||||
transaction {
|
||||
attachment(CP_PROGRAM_ID)
|
||||
attachment(CP_PROGRAM_ID)
|
||||
input(thisTest.getContract(), thisTest.getPaper())
|
||||
output(thisTest.getContract(), thisTest.getPaper())
|
||||
|
@ -567,7 +567,7 @@ class CashTests {
|
||||
|
||||
private fun makeCash(amount: Amount<Currency>, issuer: AbstractParty, depositRef: Byte = 1) =
|
||||
StateAndRef(
|
||||
TransactionState(Cash.State(amount `issued by` issuer.ref(depositRef), ourIdentity), Cash.PROGRAM_ID, DUMMY_NOTARY),
|
||||
TransactionState(Cash.State(amount `issued by` issuer.ref(depositRef), ourIdentity), Cash.PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint),
|
||||
StateRef(SecureHash.randomSHA256(), Random().nextInt(32))
|
||||
)
|
||||
|
||||
|
@ -1,21 +1,7 @@
|
||||
package net.corda.irs.contract
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.Command
|
||||
import net.corda.core.contracts.CommandData
|
||||
import net.corda.core.contracts.CommandWithParties
|
||||
import net.corda.core.contracts.Contract
|
||||
import net.corda.core.contracts.SchedulableState
|
||||
import net.corda.core.contracts.ScheduledActivity
|
||||
import net.corda.core.contracts.StateAndContract
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TransactionState
|
||||
import net.corda.core.contracts.TypeOnlyCommandData
|
||||
import net.corda.core.contracts.UniqueIdentifier
|
||||
import net.corda.core.contracts.requireThat
|
||||
import net.corda.core.contracts.select
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.flows.FlowLogicRefFactory
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
@ -254,8 +240,7 @@ class InterestRateSwap : Contract {
|
||||
* @return LocalDate or null if no more fixings.
|
||||
*/
|
||||
fun nextFixingDate(): LocalDate? {
|
||||
return floatingLegPaymentSchedule.
|
||||
filter { it.value.rate is ReferenceRate }.// TODO - a better way to determine what fixings remain to be fixed
|
||||
return floatingLegPaymentSchedule.filter { it.value.rate is ReferenceRate }.// TODO - a better way to determine what fixings remain to be fixed
|
||||
minBy { it.value.fixingDate.toEpochDay() }?.value?.fixingDate
|
||||
}
|
||||
|
||||
@ -650,7 +635,7 @@ class InterestRateSwap : Contract {
|
||||
}
|
||||
|
||||
override fun generateFix(ptx: TransactionBuilder, oldState: StateAndRef<*>, fix: Fix) {
|
||||
InterestRateSwap().generateFix(ptx, StateAndRef(TransactionState(this, IRS_PROGRAM_ID, oldState.state.notary), oldState.ref), fix)
|
||||
InterestRateSwap().generateFix(ptx, StateAndRef(TransactionState(this, IRS_PROGRAM_ID, oldState.state.notary, constraint = AlwaysAcceptAttachmentConstraint), oldState.ref), fix)
|
||||
}
|
||||
|
||||
override fun nextFixingOf(): FixOf? {
|
||||
@ -748,10 +733,9 @@ class InterestRateSwap : Contract {
|
||||
|
||||
// Put all the above into a new State object.
|
||||
val state = State(fixedLeg, floatingLeg, newCalculation, common, oracle)
|
||||
return TransactionBuilder(notary).withItems(
|
||||
StateAndContract(state, IRS_PROGRAM_ID),
|
||||
Command(Commands.Agree(), listOf(state.floatingLeg.floatingRatePayer.owningKey, state.fixedLeg.fixedRatePayer.owningKey))
|
||||
)
|
||||
return TransactionBuilder(notary)
|
||||
.addCommand(Command(Commands.Agree(), listOf(state.floatingLeg.floatingRatePayer.owningKey, state.fixedLeg.fixedRatePayer.owningKey)))
|
||||
.addOutputState(TransactionState(state, IRS_PROGRAM_ID, notary, null, AlwaysAcceptAttachmentConstraint))
|
||||
}
|
||||
|
||||
private fun calcFixingDate(date: LocalDate, fixingPeriodOffset: Int, calendar: BusinessCalendar): LocalDate {
|
||||
@ -767,7 +751,8 @@ class InterestRateSwap : Contract {
|
||||
tx.addOutputState(
|
||||
irs.state.data.copy(calculation = irs.state.data.calculation.applyFixing(fixing.of.forDay, fixedRate)),
|
||||
irs.state.contract,
|
||||
irs.state.notary
|
||||
irs.state.notary,
|
||||
constraint = AlwaysAcceptAttachmentConstraint
|
||||
)
|
||||
tx.addCommand(Commands.Refix(fixing), listOf(irs.state.data.floatingLeg.floatingRatePayer.owningKey, irs.state.data.fixedLeg.fixedRatePayer.owningKey))
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import net.corda.core.serialization.MissingAttachmentsException
|
||||
import net.corda.serialization.internal.GeneratedAttachment
|
||||
import net.corda.serialization.internal.amqp.CustomSerializer
|
||||
import net.corda.serialization.internal.amqp.SerializerFactory
|
||||
import java.security.PublicKey
|
||||
|
||||
/**
|
||||
* A serializer for [ContractAttachment] that uses a proxy object to write out the full attachment eagerly.
|
||||
@ -23,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)
|
||||
return ContractAttachmentProxy(GeneratedAttachment(bytes), obj.contract, obj.additionalContracts, obj.uploader, obj.signers)
|
||||
}
|
||||
|
||||
override fun fromProxy(proxy: ContractAttachmentProxy): ContractAttachment {
|
||||
return ContractAttachment(proxy.attachment, proxy.contract, proxy.contracts, proxy.uploader)
|
||||
return ContractAttachment(proxy.attachment, proxy.contract, proxy.contracts, proxy.uploader, proxy.signers)
|
||||
}
|
||||
|
||||
@KeepForDJVM
|
||||
data class ContractAttachmentProxy(val attachment: Attachment, val contract: ContractClassName, val contracts: Set<ContractClassName>, val uploader: String?)
|
||||
data class ContractAttachmentProxy(val attachment: Attachment, val contract: ContractClassName, val contracts: Set<ContractClassName>, val uploader: String?, val signers: List<PublicKey>)
|
||||
}
|
@ -1,11 +1,13 @@
|
||||
package net.corda.testing.contracts
|
||||
|
||||
import net.corda.core.contracts.BelongsToContract
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.identity.AbstractParty
|
||||
|
||||
/**
|
||||
* Dummy state for use in testing. Not part of any contract, not even the [DummyContract].
|
||||
*/
|
||||
@BelongsToContract(DummyContract::class)
|
||||
data class DummyState @JvmOverloads constructor (
|
||||
/** Some information that the state represents for test purposes. **/
|
||||
val magicNumber: Int = 0,
|
||||
|
@ -11,6 +11,7 @@ import net.corda.core.internal.UNKNOWN_UPLOADER
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
@ -147,8 +148,13 @@ data class TestTransactionDSLInterpreter private constructor(
|
||||
override fun _tweak(dsl: TransactionDSLInterpreter.() -> EnforceVerifyOrFail) = copy().dsl()
|
||||
|
||||
override fun _attachment(contractClassName: ContractClassName) {
|
||||
(services.cordappProvider as MockCordappProvider).addMockCordapp(contractClassName, services.attachments as MockAttachmentStorage)
|
||||
attachment((services.cordappProvider as MockCordappProvider).addMockCordapp(contractClassName, services.attachments as MockAttachmentStorage))
|
||||
}
|
||||
|
||||
override fun _attachment(contractClassName: ContractClassName, attachmentId: AttachmentId, signers: List<PublicKey>){
|
||||
attachment((services.cordappProvider as MockCordappProvider).addMockCordapp(contractClassName, services.attachments as MockAttachmentStorage, attachmentId, signers))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
data class TestLedgerDSLInterpreter private constructor(
|
||||
|
@ -1,17 +1,10 @@
|
||||
package net.corda.testing.dsl
|
||||
|
||||
import net.corda.core.DoNotImplement
|
||||
import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.contracts.AttachmentConstraint
|
||||
import net.corda.core.contracts.AutomaticHashConstraint
|
||||
import net.corda.core.contracts.CommandData
|
||||
import net.corda.core.contracts.ContractClassName
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TimeWindow
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.seconds
|
||||
import java.security.PublicKey
|
||||
@ -88,6 +81,13 @@ interface TransactionDSLInterpreter : Verifies, OutputStateLookup {
|
||||
* @param contractClassName The contract class to attach
|
||||
*/
|
||||
fun _attachment(contractClassName: ContractClassName)
|
||||
|
||||
/**
|
||||
* Attaches an attachment containing the named contract to the transaction
|
||||
* @param contractClassName The contract class to attach
|
||||
* @param attachmentId The attachment
|
||||
*/
|
||||
fun _attachment(contractClassName: ContractClassName, attachmentId: AttachmentId, signers: List<PublicKey>)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -139,37 +139,37 @@ class TransactionDSL<out T : TransactionDSLInterpreter>(interpreter: T, private
|
||||
* Adds a labelled output to the transaction.
|
||||
*/
|
||||
fun output(contractClassName: ContractClassName, label: String, notary: Party, contractState: ContractState) =
|
||||
output(contractClassName, label, notary, null, AutomaticHashConstraint, contractState)
|
||||
output(contractClassName, label, notary, null, AutomaticPlaceholderConstraint, contractState)
|
||||
|
||||
/**
|
||||
* Adds a labelled output to the transaction.
|
||||
*/
|
||||
fun output(contractClassName: ContractClassName, label: String, encumbrance: Int, contractState: ContractState) =
|
||||
output(contractClassName, label, notary, encumbrance, AutomaticHashConstraint, contractState)
|
||||
output(contractClassName, label, notary, encumbrance, AutomaticPlaceholderConstraint, contractState)
|
||||
|
||||
/**
|
||||
* Adds a labelled output to the transaction.
|
||||
*/
|
||||
fun output(contractClassName: ContractClassName, label: String, contractState: ContractState) =
|
||||
output(contractClassName, label, notary, null, AutomaticHashConstraint, contractState)
|
||||
output(contractClassName, label, notary, null, AutomaticPlaceholderConstraint, contractState)
|
||||
|
||||
/**
|
||||
* Adds an output to the transaction.
|
||||
*/
|
||||
fun output(contractClassName: ContractClassName, notary: Party, contractState: ContractState) =
|
||||
output(contractClassName, null, notary, null, AutomaticHashConstraint, contractState)
|
||||
output(contractClassName, null, notary, null, AutomaticPlaceholderConstraint, contractState)
|
||||
|
||||
/**
|
||||
* Adds an output to the transaction.
|
||||
*/
|
||||
fun output(contractClassName: ContractClassName, encumbrance: Int, contractState: ContractState) =
|
||||
output(contractClassName, null, notary, encumbrance, AutomaticHashConstraint, contractState)
|
||||
output(contractClassName, null, notary, encumbrance, AutomaticPlaceholderConstraint, contractState)
|
||||
|
||||
/**
|
||||
* Adds an output to the transaction.
|
||||
*/
|
||||
fun output(contractClassName: ContractClassName, contractState: ContractState) =
|
||||
output(contractClassName, null, notary, null, AutomaticHashConstraint, contractState)
|
||||
output(contractClassName, null, notary, null, AutomaticPlaceholderConstraint, contractState)
|
||||
|
||||
/**
|
||||
* Adds a command to the transaction.
|
||||
@ -194,5 +194,8 @@ class TransactionDSL<out T : TransactionDSLInterpreter>(interpreter: T, private
|
||||
*/
|
||||
fun attachment(contractClassName: ContractClassName) = _attachment(contractClassName)
|
||||
|
||||
fun attachment(contractClassName: ContractClassName, attachmentId: AttachmentId, signers: List<PublicKey>) = _attachment(contractClassName, attachmentId, signers)
|
||||
fun attachment(contractClassName: ContractClassName, attachmentId: AttachmentId) = _attachment(contractClassName, attachmentId, emptyList())
|
||||
|
||||
fun attachments(vararg contractClassNames: ContractClassName) = contractClassNames.forEach { attachment(it) }
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package net.corda.testing.internal
|
||||
import net.corda.core.contracts.ContractClassName
|
||||
import net.corda.core.cordapp.Cordapp
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER
|
||||
import net.corda.core.internal.cordapp.CordappImpl
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
@ -11,6 +12,7 @@ import net.corda.node.cordapp.CordappLoader
|
||||
import net.corda.node.internal.cordapp.CordappProviderImpl
|
||||
import net.corda.testing.services.MockAttachmentStorage
|
||||
import java.nio.file.Paths
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
|
||||
class MockCordappProvider(
|
||||
@ -21,7 +23,7 @@ class MockCordappProvider(
|
||||
|
||||
private val cordappRegistry = mutableListOf<Pair<Cordapp, AttachmentId>>()
|
||||
|
||||
fun addMockCordapp(contractClassName: ContractClassName, attachments: MockAttachmentStorage) {
|
||||
fun addMockCordapp(contractClassName: ContractClassName, attachments: MockAttachmentStorage, contractHash: AttachmentId? = null, signers: List<PublicKey> = emptyList()): AttachmentId {
|
||||
val cordapp = CordappImpl(
|
||||
contractClassNames = listOf(contractClassName),
|
||||
initiatedFlows = emptyList(),
|
||||
@ -36,23 +38,23 @@ class MockCordappProvider(
|
||||
info = CordappImpl.Info.UNKNOWN,
|
||||
allFlows = emptyList(),
|
||||
jarHash = SecureHash.allOnesHash)
|
||||
if (cordappRegistry.none { it.first.contractClassNames.contains(contractClassName) }) {
|
||||
cordappRegistry.add(Pair(cordapp, findOrImportAttachment(listOf(contractClassName), contractClassName.toByteArray(), attachments)))
|
||||
if (cordappRegistry.none { it.first.contractClassNames.contains(contractClassName) && it.second == contractHash }) {
|
||||
cordappRegistry.add(Pair(cordapp, findOrImportAttachment(listOf(contractClassName), contractClassName.toByteArray(), attachments, contractHash, signers)))
|
||||
}
|
||||
return cordappRegistry.findLast { contractClassName in it.first.contractClassNames }?.second!!
|
||||
}
|
||||
|
||||
override fun getContractAttachmentID(contractClassName: ContractClassName): AttachmentId? {
|
||||
return cordappRegistry.find { it.first.contractClassNames.contains(contractClassName) }?.second ?: super.getContractAttachmentID(contractClassName)
|
||||
}
|
||||
override fun getContractAttachmentID(contractClassName: ContractClassName): AttachmentId? = cordappRegistry.find { it.first.contractClassNames.contains(contractClassName) }?.second
|
||||
?: super.getContractAttachmentID(contractClassName)
|
||||
|
||||
private fun findOrImportAttachment(contractClassNames: List<ContractClassName>, data: ByteArray, attachments: MockAttachmentStorage): AttachmentId {
|
||||
val existingAttachment = attachments.files.filter {
|
||||
Arrays.equals(it.value.second, data)
|
||||
private fun findOrImportAttachment(contractClassNames: List<ContractClassName>, data: ByteArray, attachments: MockAttachmentStorage, contractHash: AttachmentId?, signers: List<PublicKey>): AttachmentId {
|
||||
val existingAttachment = attachments.files.filter { (attachmentId, content) ->
|
||||
contractHash == attachmentId
|
||||
}
|
||||
return if (!existingAttachment.isEmpty()) {
|
||||
existingAttachment.keys.first()
|
||||
} else {
|
||||
attachments.importContractAttachment(contractClassNames, DEPLOYED_CORDAPP_UPLOADER, data.inputStream())
|
||||
attachments.importContractAttachment(contractClassNames, DEPLOYED_CORDAPP_UPLOADER, data.inputStream(), contractHash, signers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -103,7 +103,7 @@ class VaultFiller @JvmOverloads constructor(
|
||||
linearNumber: Long = 0L,
|
||||
linearBoolean: Boolean = false,
|
||||
linearTimestamp: Instant = now(),
|
||||
constraint: AttachmentConstraint = AutomaticHashConstraint): Vault<LinearState> {
|
||||
constraint: AttachmentConstraint = AutomaticPlaceholderConstraint): Vault<LinearState> {
|
||||
val myKey: PublicKey = services.myInfo.chooseIdentity().owningKey
|
||||
val me = AnonymousParty(myKey)
|
||||
val issuerKey = defaultNotary.keyPair
|
||||
|
@ -5,7 +5,9 @@ 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
|
||||
@ -15,6 +17,7 @@ import net.corda.core.node.services.vault.AttachmentSort
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.nodeapi.internal.withContractsInJar
|
||||
import java.io.InputStream
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
import java.util.jar.JarInputStream
|
||||
|
||||
@ -53,22 +56,23 @@ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() {
|
||||
}
|
||||
}
|
||||
|
||||
fun importContractAttachment(contractClassNames: List<ContractClassName>, uploader: String, jar: InputStream): AttachmentId = importAttachmentInternal(jar, uploader, contractClassNames)
|
||||
@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 getAttachmentIdAndBytes(jar: InputStream): Pair<AttachmentId, ByteArray> = jar.readFully().let { bytes -> Pair(bytes.sha256(), bytes) }
|
||||
|
||||
private class MockAttachment(dataLoader: () -> ByteArray, override val id: SecureHash) : AbstractAttachment(dataLoader)
|
||||
private class MockAttachment(dataLoader: () -> ByteArray, override val id: SecureHash, override val signers: List<PublicKey>) : AbstractAttachment(dataLoader)
|
||||
|
||||
private fun importAttachmentInternal(jar: InputStream, uploader: String, contractClassNames: List<ContractClassName>? = null): AttachmentId {
|
||||
private fun importAttachmentInternal(jar: InputStream, uploader: String, contractClassNames: List<ContractClassName>? = null, attachmentId: AttachmentId? = null, signers: List<PublicKey> = emptyList()): AttachmentId {
|
||||
// JIS makes read()/readBytes() return bytes of the current file, but we want to hash the entire container here.
|
||||
require(jar !is JarInputStream)
|
||||
|
||||
val bytes = jar.readFully()
|
||||
|
||||
val sha256 = bytes.sha256()
|
||||
val sha256 = attachmentId ?: bytes.sha256()
|
||||
if (sha256 !in files.keys) {
|
||||
val baseAttachment = MockAttachment({ bytes }, sha256)
|
||||
val attachment = if (contractClassNames == null || contractClassNames.isEmpty()) baseAttachment else ContractAttachment(baseAttachment, contractClassNames.first(), contractClassNames.toSet(), uploader)
|
||||
val baseAttachment = MockAttachment({ bytes }, sha256, signers)
|
||||
val attachment = if (contractClassNames == null || contractClassNames.isEmpty()) baseAttachment else ContractAttachment(baseAttachment, contractClassNames.first(), contractClassNames.toSet(), uploader, signers)
|
||||
_files[sha256] = Pair(attachment, bytes)
|
||||
}
|
||||
return sha256
|
||||
|
Loading…
x
Reference in New Issue
Block a user