CORDA-941 - Add Network Parameters contract implementation Whitelist (#2539)

* CORDA-941 Add Network Parameters contract implementation Whitelist

* CORDA-941 fix merge

* CORDA-941 added uploader support to Attachments and added check into the AttachmentClassloader to only allow loading attachments that were uploaded from the local cordapps folder

* CORDA-941 update api spec

* CORDA-941 address some code review changes and fix and add classloader test

* CORDA-941 fix test

* CORDA-941 address code review comments

* CORDA-941 address code review comments - use and update existing whitelist

* CORDA-941 address code review comments

* CORDA-941 fix compile error

* CORDA-941 address code review comments

* CORDA-941 removed: whitelistAllContractsForTest

* CORDA-941 removed: whitelistAllContractsForTest

* CORDA-941 add comment

* CORDA-941 remove toLedgerTransaction

* CORDA-941 add warning when node not using latest CorDapp

* CORDA-941 remove the stubbing approach for cleaning LedgerTransaction

* CORDA-941 Code review changes

* CORDA-941 Fix merge

* CORDA-941 workaround for api scanner bug

* Fixed JacksonSupportTest.
This commit is contained in:
Tudor Malene 2018-02-20 11:09:23 +00:00 committed by Katelyn Baker
parent 34e82026f3
commit 60a4bcba5b
50 changed files with 539 additions and 235 deletions

View File

@ -209,7 +209,7 @@ public static final class net.corda.core.context.Trace$InvocationId$Companion ex
public static final class net.corda.core.context.Trace$SessionId$Companion extends java.lang.Object
@kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.core.context.Trace$SessionId newInstance(String, java.time.Instant)
##
@net.corda.core.serialization.CordaSerializable public final class net.corda.core.contracts.AlwaysAcceptAttachmentConstraint extends java.lang.Object implements net.corda.core.contracts.AttachmentConstraint
@net.corda.core.serialization.CordaSerializable @net.corda.core.DoNotImplement public final class net.corda.core.contracts.AlwaysAcceptAttachmentConstraint extends java.lang.Object implements net.corda.core.contracts.AttachmentConstraint
public boolean isSatisfiedBy(net.corda.core.contracts.Attachment)
public static final net.corda.core.contracts.AlwaysAcceptAttachmentConstraint INSTANCE
##
@ -284,14 +284,14 @@ public static final class net.corda.core.contracts.AmountTransfer$Companion exte
@org.jetbrains.annotations.NotNull public abstract java.io.InputStream open()
@org.jetbrains.annotations.NotNull public abstract jar.JarInputStream openAsJAR()
##
@net.corda.core.serialization.CordaSerializable public interface net.corda.core.contracts.AttachmentConstraint
@net.corda.core.serialization.CordaSerializable @net.corda.core.DoNotImplement public interface net.corda.core.contracts.AttachmentConstraint
public abstract boolean isSatisfiedBy(net.corda.core.contracts.Attachment)
##
@net.corda.core.serialization.CordaSerializable public final class net.corda.core.contracts.AttachmentResolutionException extends net.corda.core.flows.FlowException
public <init>(net.corda.core.crypto.SecureHash)
@org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getHash()
##
@net.corda.core.serialization.CordaSerializable public final class net.corda.core.contracts.AutomaticHashConstraint extends java.lang.Object implements net.corda.core.contracts.AttachmentConstraint
@net.corda.core.serialization.CordaSerializable @net.corda.core.DoNotImplement public final class net.corda.core.contracts.AutomaticHashConstraint extends java.lang.Object implements net.corda.core.contracts.AttachmentConstraint
public boolean isSatisfiedBy(net.corda.core.contracts.Attachment)
public static final net.corda.core.contracts.AutomaticHashConstraint INSTANCE
##
@ -338,19 +338,36 @@ public final class net.corda.core.contracts.ComponentGroupEnum extends java.lang
public static net.corda.core.contracts.ComponentGroupEnum valueOf(String)
public static net.corda.core.contracts.ComponentGroupEnum[] values()
##
@net.corda.core.serialization.CordaSerializable public final class net.corda.core.contracts.ConstraintAttachment extends java.lang.Object implements net.corda.core.contracts.Attachment
public <init>(net.corda.core.contracts.ContractAttachment, String)
public void extractFile(String, java.io.OutputStream)
@org.jetbrains.annotations.NotNull public final net.corda.core.contracts.ContractAttachment getContractAttachment()
@org.jetbrains.annotations.NotNull public net.corda.core.crypto.SecureHash getId()
@org.jetbrains.annotations.NotNull public List getSigners()
public int getSize()
@org.jetbrains.annotations.NotNull public final String getStateContract()
@org.jetbrains.annotations.NotNull public java.io.InputStream open()
@org.jetbrains.annotations.NotNull public jar.JarInputStream openAsJAR()
##
@net.corda.core.serialization.CordaSerializable public interface net.corda.core.contracts.Contract
public abstract void verify(net.corda.core.transactions.LedgerTransaction)
##
@net.corda.core.serialization.CordaSerializable public final class net.corda.core.contracts.ContractAttachment extends java.lang.Object implements net.corda.core.contracts.Attachment
public <init>(net.corda.core.contracts.Attachment, String)
public <init>(net.corda.core.contracts.Attachment, String, Set)
public <init>(net.corda.core.contracts.Attachment, String, Set, String)
public void extractFile(String, java.io.OutputStream)
@org.jetbrains.annotations.NotNull public final Set getAdditionalContracts()
@org.jetbrains.annotations.NotNull public final Set getAllContracts()
@org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Attachment getAttachment()
@org.jetbrains.annotations.NotNull public final String getContract()
@org.jetbrains.annotations.NotNull public net.corda.core.crypto.SecureHash getId()
@org.jetbrains.annotations.NotNull public List getSigners()
public int getSize()
@org.jetbrains.annotations.Nullable public final String getUploader()
@org.jetbrains.annotations.NotNull public java.io.InputStream open()
@org.jetbrains.annotations.NotNull public jar.JarInputStream openAsJAR()
@org.jetbrains.annotations.NotNull public String toString()
##
@net.corda.core.serialization.CordaSerializable public interface net.corda.core.contracts.ContractState
@org.jetbrains.annotations.NotNull public abstract List getParticipants()
@ -366,7 +383,7 @@ public final class net.corda.core.contracts.ContractsDSL extends java.lang.Objec
@org.jetbrains.annotations.NotNull public abstract Collection getExitKeys()
@org.jetbrains.annotations.NotNull public abstract net.corda.core.contracts.FungibleAsset withNewOwnerAndAmount(net.corda.core.contracts.Amount, net.corda.core.identity.AbstractParty)
##
@net.corda.core.serialization.CordaSerializable public final class net.corda.core.contracts.HashAttachmentConstraint extends java.lang.Object implements net.corda.core.contracts.AttachmentConstraint
@net.corda.core.serialization.CordaSerializable @net.corda.core.DoNotImplement public final class net.corda.core.contracts.HashAttachmentConstraint extends java.lang.Object implements net.corda.core.contracts.AttachmentConstraint
public <init>(net.corda.core.crypto.SecureHash)
@org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash component1()
@org.jetbrains.annotations.NotNull public final net.corda.core.contracts.HashAttachmentConstraint copy(net.corda.core.crypto.SecureHash)
@ -557,6 +574,9 @@ public final class net.corda.core.contracts.TransactionStateKt extends java.lang
@net.corda.core.serialization.CordaSerializable public abstract class net.corda.core.contracts.TransactionVerificationException extends net.corda.core.flows.FlowException
@org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getTxId()
##
@net.corda.core.serialization.CordaSerializable public static final class net.corda.core.contracts.TransactionVerificationException$ConflictingAttachmentsRejection extends net.corda.core.contracts.TransactionVerificationException
public <init>(net.corda.core.crypto.SecureHash, String)
##
@net.corda.core.serialization.CordaSerializable public static final class net.corda.core.contracts.TransactionVerificationException$ContractConstraintRejection extends net.corda.core.contracts.TransactionVerificationException
public <init>(net.corda.core.crypto.SecureHash, String)
##
@ -620,6 +640,10 @@ public static final class net.corda.core.contracts.UniqueIdentifier$Companion ex
@org.jetbrains.annotations.NotNull public abstract String getLegacyContract()
@org.jetbrains.annotations.NotNull public abstract net.corda.core.contracts.ContractState upgrade(net.corda.core.contracts.ContractState)
##
@net.corda.core.serialization.CordaSerializable @net.corda.core.DoNotImplement public final class net.corda.core.contracts.WhitelistedByZoneAttachmentConstraint extends java.lang.Object implements net.corda.core.contracts.AttachmentConstraint
public <init>(Map)
public boolean isSatisfiedBy(net.corda.core.contracts.Attachment)
##
@net.corda.core.DoNotImplement public interface net.corda.core.cordapp.Cordapp
@org.jetbrains.annotations.NotNull public abstract List getContractClassNames()
@org.jetbrains.annotations.NotNull public abstract List getCordappClasses()
@ -1745,14 +1769,15 @@ public @interface net.corda.core.messaging.RPCReturnsObservables
@org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.FlowProgressHandle startTrackedFlow(net.corda.core.flows.FlowLogic)
##
@net.corda.core.serialization.CordaSerializable public final class net.corda.core.node.NetworkParameters extends java.lang.Object
public <init>(int, List, int, int, java.time.Instant, int)
public <init>(int, List, int, int, java.time.Instant, int, Map)
public final int component1()
@org.jetbrains.annotations.NotNull public final List component2()
public final int component3()
public final int component4()
@org.jetbrains.annotations.NotNull public final java.time.Instant component5()
public final int component6()
@org.jetbrains.annotations.NotNull public final net.corda.core.node.NetworkParameters copy(int, List, int, int, java.time.Instant, int)
@org.jetbrains.annotations.NotNull public final Map component7()
@org.jetbrains.annotations.NotNull public final net.corda.core.node.NetworkParameters copy(int, List, int, int, java.time.Instant, int, Map)
public boolean equals(Object)
public final int getEpoch()
public final int getMaxMessageSize()
@ -1760,6 +1785,7 @@ public @interface net.corda.core.messaging.RPCReturnsObservables
public final int getMinimumPlatformVersion()
@org.jetbrains.annotations.NotNull public final java.time.Instant getModifiedTime()
@org.jetbrains.annotations.NotNull public final List getNotaries()
@org.jetbrains.annotations.NotNull public final Map getWhitelistedContractImplementations()
public int hashCode()
public String toString()
##
@ -1836,9 +1862,9 @@ public final class net.corda.core.node.StatesToRecord extends java.lang.Enum
##
@net.corda.core.DoNotImplement public interface net.corda.core.node.services.AttachmentStorage
public abstract boolean hasAttachment(net.corda.core.crypto.SecureHash)
@org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.SecureHash importAttachment(java.io.InputStream)
@kotlin.Deprecated @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.SecureHash importAttachment(java.io.InputStream)
@org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.SecureHash importAttachment(java.io.InputStream, String, String)
@org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.SecureHash importOrGetAttachment(java.io.InputStream)
@kotlin.Deprecated @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.SecureHash importOrGetAttachment(java.io.InputStream)
@org.jetbrains.annotations.Nullable public abstract net.corda.core.contracts.Attachment openAttachment(net.corda.core.crypto.SecureHash)
@org.jetbrains.annotations.NotNull public abstract List queryAttachments(net.corda.core.node.services.vault.AttachmentQueryCriteria, net.corda.core.node.services.vault.AttachmentSort)
##
@ -4645,6 +4671,7 @@ public final class net.corda.testing.services.MockAttachmentStorage extends net.
public boolean hasAttachment(net.corda.core.crypto.SecureHash)
@org.jetbrains.annotations.NotNull public net.corda.core.crypto.SecureHash importAttachment(java.io.InputStream)
@org.jetbrains.annotations.NotNull public net.corda.core.crypto.SecureHash importAttachment(java.io.InputStream, String, String)
@org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash importContractAttachment(List, String, java.io.InputStream)
@org.jetbrains.annotations.NotNull public net.corda.core.crypto.SecureHash importOrGetAttachment(java.io.InputStream)
@org.jetbrains.annotations.Nullable public net.corda.core.contracts.Attachment openAttachment(net.corda.core.crypto.SecureHash)
@org.jetbrains.annotations.NotNull public List queryAttachments(net.corda.core.node.services.vault.AttachmentQueryCriteria, net.corda.core.node.services.vault.AttachmentSort)

View File

@ -10,6 +10,7 @@ import net.corda.core.identity.CordaX500Name
import net.corda.core.node.ServiceHub
import net.corda.core.transactions.SignedTransaction
import net.corda.finance.USD
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.contracts.DummyContract
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.DUMMY_NOTARY_NAME
@ -101,6 +102,7 @@ class JacksonSupportTest {
fun writeTransaction() {
val attachmentRef = SecureHash.randomSHA256()
doReturn(attachmentRef).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID)
doReturn(testNetworkParameters()).whenever(services).networkParameters
fun makeDummyTx(): SignedTransaction {
val wtx = DummyContract.generateInitial(1, DUMMY_NOTARY, MINI_CORP.ref(1))
.toWireTransaction(services)

View File

@ -1,10 +1,13 @@
package net.corda.core.contracts
import net.corda.core.DoNotImplement
import net.corda.core.crypto.SecureHash
import net.corda.core.node.services.AttachmentId
import net.corda.core.serialization.CordaSerializable
/** Constrain which contract-code-containing attachment can be used with a [ContractState]. */
@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
@ -20,6 +23,25 @@ data class HashAttachmentConstraint(val attachmentId: SecureHash) : AttachmentCo
override fun isSatisfiedBy(attachment: Attachment) = attachment.id == attachmentId
}
/**
* An [AttachmentConstraint] that verifies that the hash of the attachment is in the network parameters whitelist.
* See: [net.corda.core.node.NetworkParameters.whitelistedContractImplementations]
* It allows for centralized control over the cordapps that can be used.
*
* @param whitelistedContractImplementations whitelisted attachment IDs by contract class name.
*/
class WhitelistedByZoneAttachmentConstraint(private val whitelistedContractImplementations: Map<String, List<AttachmentId>>) : AttachmentConstraint {
override fun isSatisfiedBy(attachment: Attachment): Boolean {
return whitelistedContractImplementations.let { whitelist ->
when (attachment) {
is ConstraintAttachment -> 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
@ -35,3 +57,15 @@ object AutomaticHashConstraint : AttachmentConstraint {
throw UnsupportedOperationException("Contracts cannot be satisfied by an AutomaticHashConstraint placeholder")
}
}
/**
* Used only for passing to the Attachment constraint verification.
* Encapsulates a [ContractAttachment] and the state contract
*/
class ConstraintAttachment(val contractAttachment: ContractAttachment, val stateContract: ContractClassName) : Attachment by contractAttachment {
init {
require(stateContract in contractAttachment.allContracts) {
"This ConstraintAttachment was not initialised properly"
}
}
}

View File

@ -6,7 +6,15 @@ import net.corda.core.serialization.CordaSerializable
* Wrap an attachment in this if it is to be used as an executable contract attachment
*
* @property attachment The attachment representing the contract JAR
* @property contract The contract name contained within the JAR
* @property contract The contract name contained within the JAR. A Contract attachment has to contain at least 1 contract.
* @property additionalContracts Additional contract names contained within the JAR.
*/
@CordaSerializable
class ContractAttachment(val attachment: Attachment, val contract: ContractClassName) : Attachment by attachment
class ContractAttachment @JvmOverloads constructor (val attachment: Attachment, val contract: ContractClassName, val additionalContracts: Set<ContractClassName> = emptySet(), val uploader: String? = null) : Attachment by attachment {
val allContracts: Set<ContractClassName> get() = additionalContracts + contract
override fun toString(): String {
return "ContractAttachment(attachment=${attachment.id}, contracts='${allContracts}', uploader='${uploader}')"
}
}

View File

@ -22,6 +22,9 @@ sealed class TransactionVerificationException(val txId: SecureHash, message: Str
class MissingAttachmentRejection(txId: SecureHash, val contractClass: String)
: TransactionVerificationException(txId, "Contract constraints failed, could not find attachment for: $contractClass", null)
class ConflictingAttachmentsRejection(txId: SecureHash, contractClass: String)
: TransactionVerificationException(txId, "Contract constraints failed for: $contractClass, because multiple attachments providing this contract were attached.", null)
class ContractCreationError(txId: SecureHash, contractClass: String, cause: Throwable)
: TransactionVerificationException(txId, "Contract verification failed: ${cause.message}, could not create contract class: $contractClass", cause)

View File

@ -13,6 +13,13 @@ import java.security.CodeSigner
import java.security.cert.X509Certificate
import java.util.jar.JarInputStream
// Possible attachment uploaders
const val DEPLOYED_CORDAPP_UPLOADER = "app"
const val RPC_UPLOADER = "rpc"
const val TEST_UPLOADER = "test"
const val P2P_UPLOADER = "p2p"
const val UNKNOWN_UPLOADER = "unknown"
abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment {
companion object {
fun SerializeAsTokenContext.attachmentDataLoader(id: SecureHash): () -> ByteArray {

View File

@ -147,7 +147,7 @@ class FetchAttachmentsFlow(requests: Set<SecureHash>,
override fun maybeWriteToDisk(downloaded: List<Attachment>) {
for (attachment in downloaded) {
serviceHub.attachments.importAttachment(attachment.open())
serviceHub.attachments.importAttachment(attachment.open(), "$P2P_UPLOADER:${otherSideSession.counterparty.name}", null)
}
}

View File

@ -2,7 +2,6 @@
package net.corda.core.internal
import net.corda.core.cordapp.CordappProvider
import net.corda.core.crypto.*
import net.corda.core.identity.CordaX500Name
import net.corda.core.node.ServicesForResolution
@ -296,8 +295,8 @@ fun <T, U : T> uncheckedCast(obj: T) = obj as U
fun <K, V> Iterable<Pair<K, V>>.toMultiMap(): Map<K, List<V>> = this.groupBy({ it.first }) { it.second }
/** Provide access to internal method for AttachmentClassLoaderTests */
fun TransactionBuilder.toWireTransaction(cordappProvider: CordappProvider, serializationContext: SerializationContext): WireTransaction {
return toWireTransactionWithContext(cordappProvider, serializationContext)
fun TransactionBuilder.toWireTransaction(services: ServicesForResolution, serializationContext: SerializationContext): WireTransaction {
return toWireTransactionWithContext(services, serializationContext)
}
/** Provide access to internal method for AttachmentClassLoaderTests */

View File

@ -1,6 +1,7 @@
package net.corda.core.node
import net.corda.core.identity.Party
import net.corda.core.node.services.AttachmentId
import net.corda.core.serialization.CordaSerializable
import java.time.Instant
@ -14,6 +15,8 @@ import java.time.Instant
* @property modifiedTime Last modification time of network parameters set.
* @property epoch Version number of the network parameters. Starting from 1, this will always increment on each new set
* 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]. Read more about contract constraints here: <https://docs.corda.net/api-contract-constraints.html>
*/
// TODO Add eventHorizon - how many days a node can be offline before being automatically ejected from the network.
// It needs separate design.
@ -24,7 +27,8 @@ data class NetworkParameters(
val maxMessageSize: Int,
val maxTransactionSize: Int,
val modifiedTime: Instant,
val epoch: Int
val epoch: Int,
val whitelistedContractImplementations: Map<String, List<AttachmentId>>
) {
init {
require(minimumPlatformVersion > 0) { "minimumPlatformVersion must be at least 1" }

View File

@ -33,6 +33,7 @@ interface AttachmentStorage {
* @throws IllegalArgumentException if the given byte stream is empty or a [java.util.jar.JarInputStream].
* @throws IOException if something went wrong.
*/
@Deprecated("More attachment information is required", replaceWith = ReplaceWith("importAttachment(jar, uploader, filename)"))
@Throws(FileAlreadyExistsException::class, IOException::class)
fun importAttachment(jar: InputStream): AttachmentId
@ -43,13 +44,14 @@ interface AttachmentStorage {
* @param filename Name of the file
*/
@Throws(FileAlreadyExistsException::class, IOException::class)
fun importAttachment(jar: InputStream, uploader: String, filename: String): AttachmentId
fun importAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId
/**
* Inserts or returns Attachment Id of attachment. Does not throw an exception if already uploaded.
* @param jar [InputStream] of Jar file
* @return [AttachmentId] of uploaded attachment
*/
@Deprecated("More attachment information is required", replaceWith = ReplaceWith("importAttachment(jar, uploader, filename)"))
fun importOrGetAttachment(jar: InputStream): AttachmentId
/**

View File

@ -87,17 +87,28 @@ data class LedgerTransaction(
/**
* Verify that all contract constraints are valid for each state before running any contract code
*
* 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.
*
* @throws TransactionVerificationException if the constraints fail to verify
*/
private fun verifyConstraints() {
val contractAttachments = attachments.filterIsInstance<ContractAttachment>()
(inputs.map { it.state } + outputs).forEach { state ->
// Ordering of attachments matters - if two attachments contain the same named contract then the second
// will be shadowed by the first.
val contractAttachment = contractAttachments.find { it.contract == state.contract }
?: throw TransactionVerificationException.MissingAttachmentRejection(id, state.contract)
val stateAttachments = contractAttachments.filter { state.contract in it.allContracts }
if (stateAttachments.isEmpty()) throw TransactionVerificationException.MissingAttachmentRejection(id, state.contract)
if (!state.constraint.isSatisfiedBy(contractAttachment)) {
val uniqueAttachmentsForStateContract = stateAttachments.distinctBy { it.id }
// 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
if (uniqueAttachmentsForStateContract.size > 1) {
throw TransactionVerificationException.ConflictingAttachmentsRejection(id, state.contract)
}
val contractAttachment = uniqueAttachmentsForStateContract.first()
if (!state.constraint.isSatisfiedBy(ConstraintAttachment(contractAttachment, state.contract))) {
throw TransactionVerificationException.ContractConstraintRejection(id, state.contract)
}
}

View File

@ -8,8 +8,10 @@ import net.corda.core.crypto.SignableData
import net.corda.core.crypto.SignatureMetadata
import net.corda.core.identity.Party
import net.corda.core.internal.FlowStateMachine
import net.corda.core.node.NetworkParameters
import net.corda.core.node.ServiceHub
import net.corda.core.node.ServicesForResolution
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
@ -17,6 +19,7 @@ import java.security.PublicKey
import java.time.Duration
import java.time.Instant
import java.util.*
import kotlin.collections.ArrayList
/**
* A TransactionBuilder is a transaction class that's mutable (unlike the others which are all immutable). It is
@ -42,18 +45,24 @@ open class TransactionBuilder(
) {
constructor(notary: Party) : this(notary, (Strand.currentStrand() as? FlowStateMachine<*>)?.id?.uuid ?: UUID.randomUUID())
private val inputsWithTransactionState = arrayListOf<TransactionState<ContractState>>()
/**
* Creates a copy of the builder.
*/
fun copy() = TransactionBuilder(
notary = notary,
inputs = ArrayList(inputs),
attachments = ArrayList(attachments),
outputs = ArrayList(outputs),
commands = ArrayList(commands),
window = window,
privacySalt = privacySalt
)
fun copy(): TransactionBuilder {
val t = TransactionBuilder(
notary = notary,
inputs = ArrayList(inputs),
attachments = ArrayList(attachments),
outputs = ArrayList(outputs),
commands = ArrayList(commands),
window = window,
privacySalt = privacySalt
)
t.inputsWithTransactionState.addAll(this.inputsWithTransactionState)
return t
}
// DOCSTART 1
/** A more convenient way to add items to this transaction that calls the add* methods for you based on type */
@ -83,31 +92,52 @@ open class TransactionBuilder(
* @returns A new [WireTransaction] that will be unaffected by further changes to this [TransactionBuilder].
*/
@Throws(MissingContractAttachments::class)
fun toWireTransaction(services: ServicesForResolution): WireTransaction = toWireTransactionWithContext(services.cordappProvider)
fun toWireTransaction(services: ServicesForResolution): WireTransaction = toWireTransactionWithContext(services)
internal fun toWireTransactionWithContext(cordappProvider: CordappProvider, serializationContext: SerializationContext? = null): WireTransaction {
// Resolves the AutomaticHashConstraints to HashAttachmentConstraints for convenience. The AutomaticHashConstraint
// allows for less boiler plate when constructing transactions since for the typical case the named contract
internal fun toWireTransactionWithContext(services: ServicesForResolution, serializationContext: SerializationContext? = null): WireTransaction {
// Resolves the AutomaticHashConstraints to HashAttachmentConstraints or WhitelistedByZoneAttachmentConstraint 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 ->
if (state.constraint is AutomaticHashConstraint) {
cordappProvider.getContractAttachmentID(state.contract)?.let {
when {
state.constraint !is AutomaticHashConstraint -> state
useWhitelistedByZoneAttachmentConstraint(state.contract, services.networkParameters) -> state.copy(constraint = WhitelistedByZoneAttachmentConstraint(services.networkParameters.whitelistedContractImplementations))
else -> services.cordappProvider.getContractAttachmentID(state.contract)?.let {
state.copy(constraint = HashAttachmentConstraint(it))
} ?: throw MissingContractAttachments(listOf(state))
} else {
state
}
}
return SerializationFactory.defaultFactory.withCurrentContext(serializationContext) {
WireTransaction(WireTransaction.createComponentGroups(inputs, resolvedOutputs, commands, attachments, notary, window), privacySalt)
WireTransaction(WireTransaction.createComponentGroups(inputStates(), resolvedOutputs, commands, attachments + makeContractAttachments(services.cordappProvider), notary, window), privacySalt)
}
}
private fun useWhitelistedByZoneAttachmentConstraint(contractClassName: ContractClassName, networkParameters: NetworkParameters): Boolean {
return contractClassName in networkParameters.whitelistedContractImplementations.keys
}
/**
* 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
*/
private fun makeContractAttachments(cordappProvider: CordappProvider): List<AttachmentId> {
return (inputsWithTransactionState + outputs).map { state ->
cordappProvider.getContractAttachmentID(state.contract)
?: throw MissingContractAttachments(listOf(state))
}.distinct()
}
@Throws(AttachmentResolutionException::class, TransactionResolutionException::class)
fun toLedgerTransaction(services: ServiceHub) = toWireTransaction(services).toLedgerTransaction(services)
internal fun toLedgerTransactionWithContext(services: ServicesForResolution, serializationContext: SerializationContext) = toWireTransactionWithContext(services.cordappProvider, serializationContext).toLedgerTransaction(services)
internal fun toLedgerTransactionWithContext(services: ServicesForResolution, serializationContext: SerializationContext): LedgerTransaction {
return toWireTransactionWithContext(services, serializationContext).toLedgerTransaction(services)
}
@Throws(AttachmentResolutionException::class, TransactionResolutionException::class, TransactionVerificationException::class)
fun verify(services: ServiceHub) {
toLedgerTransaction(services).verify()
@ -117,6 +147,7 @@ open class TransactionBuilder(
val notary = stateAndRef.state.notary
require(notary == this.notary) { "Input state requires notary \"$notary\" which does not match the transaction notary \"${this.notary}\"." }
inputs.add(stateAndRef.ref)
inputsWithTransactionState.add(stateAndRef.state)
return this
}

View File

@ -88,7 +88,6 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
resolveIdentity = { services.identityService.partyFromKey(it) },
resolveAttachment = { services.attachments.openAttachment(it) },
resolveStateRef = { services.loadState(it) },
resolveContractAttachment = { services.cordappProvider.getContractAttachmentID(it.contract) },
maxTransactionSize = services.networkParameters.maxTransactionSize
)
}
@ -108,17 +107,16 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
resolveStateRef: (StateRef) -> TransactionState<*>?,
resolveContractAttachment: (TransactionState<ContractState>) -> AttachmentId?
): LedgerTransaction {
return toLedgerTransactionInternal(resolveIdentity, resolveAttachment, resolveStateRef, resolveContractAttachment, 10485760)
return toLedgerTransactionInternal(resolveIdentity, resolveAttachment, resolveStateRef, 10485760)
}
private fun toLedgerTransactionInternal(
resolveIdentity: (PublicKey) -> Party?,
resolveAttachment: (SecureHash) -> Attachment?,
resolveStateRef: (StateRef) -> TransactionState<*>?,
resolveContractAttachment: (TransactionState<ContractState>) -> AttachmentId?,
maxTransactionSize: Int
): LedgerTransaction {
// Look up public keys to authenticated identities. This is just a stub placeholder and will all change in future.
// Look up public keys to authenticated identities.
val authenticatedArgs = commands.map {
val parties = it.signers.mapNotNull { pk -> resolveIdentity(pk) }
CommandWithParties(it.signers, parties, it.value)
@ -126,10 +124,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
val resolvedInputs = inputs.map { ref ->
resolveStateRef(ref)?.let { StateAndRef(it, ref) } ?: throw TransactionResolutionException(ref.txhash)
}
// Open attachments specified in this transaction. If we haven't downloaded them, we fail.
val contractAttachments = findAttachmentContracts(resolvedInputs, resolveContractAttachment, resolveAttachment)
// Order of attachments is important since contracts may refer to indexes so only append automatic attachments
val attachments = (attachments.map { resolveAttachment(it) ?: throw AttachmentResolutionException(it) } + contractAttachments).distinct()
val attachments = attachments.map { resolveAttachment(it) ?: throw AttachmentResolutionException(it) }
val ltx = LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, notary, timeWindow, privacySalt)
checkTransactionSize(ltx, maxTransactionSize)
return ltx
@ -265,19 +260,6 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
return buf.toString()
}
private fun findAttachmentContracts(resolvedInputs: List<StateAndRef<ContractState>>,
resolveContractAttachment: (TransactionState<ContractState>) -> AttachmentId?,
resolveAttachment: (SecureHash) -> Attachment?
): List<Attachment> {
val contractAttachments = (outputs + resolvedInputs.map { it.state }).map { Pair(it, resolveContractAttachment(it)) }
val missingAttachments = contractAttachments.filter { it.second == null }
return if (missingAttachments.isEmpty()) {
contractAttachments.map { ContractAttachment(resolveAttachment(it.second!!) ?: throw AttachmentResolutionException(it.second!!), it.first.contract) }
} else {
throw MissingContractAttachments(missingAttachments.map { it.first })
}
}
override fun equals(other: Any?): Boolean {
if (other is WireTransaction) {
return (this.id == other.id)

View File

@ -1,9 +1,11 @@
package net.corda.core.contracts
import com.nhaarman.mockito_kotlin.any
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.cordapp.CordappProvider
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.SecureHash.Companion.allOnesHash
import net.corda.core.internal.UpgradeCommand
import net.corda.core.node.ServicesForResolution
import net.corda.testing.contracts.DummyContract
@ -34,7 +36,9 @@ class DummyContractV2Tests {
@Test
fun `upgrade from v1`() {
val services = rigorousMock<ServicesForResolution>().also {
doReturn(rigorousMock<CordappProvider>()).whenever(it).cordappProvider
doReturn(rigorousMock<CordappProvider>().also {
doReturn(allOnesHash).whenever(it).getContractAttachmentID(any())
}).whenever(it).cordappProvider
}
val contractUpgrade = DummyContractV2()
val v1State = TransactionState(DummyContract.SingleOwnerState(0, ALICE), DummyContract.PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint)

View File

@ -10,7 +10,6 @@ import net.corda.core.identity.Party
import net.corda.node.services.api.IdentityServiceInternal
import net.corda.testing.contracts.DummyContract
import net.corda.testing.core.*
import net.corda.testing.internal.MockCordappProvider
import net.corda.testing.internal.rigorousMock
import net.corda.testing.node.MockServices
import org.junit.Before
@ -30,7 +29,7 @@ class LedgerTransactionQueryTests {
@JvmField
val testSerialization = SerializationEnvironmentRule()
private val keyPair = generateKeyPair()
private val services = MockServices(emptyList(), CordaX500Name("MegaCorp", "London", "GB"),
private val services = MockServices(listOf("net.corda.testing.contracts"), CordaX500Name("MegaCorp", "London", "GB"),
rigorousMock<IdentityServiceInternal>().also {
doReturn(null).whenever(it).partyFromKey(keyPair.public)
}, keyPair)

View File

@ -65,7 +65,7 @@ class TransactionEncumbranceTests {
}
}
private val ledgerServices = MockServices(emptyList(), MEGA_CORP.name,
private val ledgerServices = MockServices(listOf("net.corda.core.transactions", "net.corda.finance.contracts.asset"), MEGA_CORP.name,
rigorousMock<IdentityServiceInternal>().also {
doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY)
})

View File

@ -1,5 +1,7 @@
package net.corda.core.transactions
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.contracts.*
import net.corda.core.crypto.*
import net.corda.core.crypto.CompositeKey
@ -112,7 +114,9 @@ class TransactionTests {
val inputs = emptyList<StateAndRef<*>>()
val outputs = listOf(baseOutState, baseOutState.copy(notary = ALICE), baseOutState.copy(notary = BOB))
val commands = emptyList<CommandWithParties<CommandData>>()
val attachments = listOf<Attachment>(ContractAttachment(rigorousMock(), DummyContract.PROGRAM_ID))
val attachments = listOf<Attachment>(ContractAttachment(rigorousMock<Attachment>().also {
doReturn(SecureHash.zeroHash).whenever(it).id
}, DummyContract.PROGRAM_ID))
val id = SecureHash.randomSHA256()
val timeWindow: TimeWindow? = null
val privacySalt: PrivacySalt = PrivacySalt()

View File

@ -7,24 +7,22 @@ import net.corda.core.identity.CordaX500Name;
import net.corda.finance.contracts.ICommercialPaperState;
import net.corda.finance.contracts.JavaCommercialPaper;
import net.corda.finance.contracts.asset.Cash;
import net.corda.testing.node.MockServices;
import net.corda.testing.core.TestIdentity;
import net.corda.testing.node.MockServices;
import org.junit.Before;
import org.junit.Test;
import java.security.PublicKey;
import java.time.temporal.ChronoUnit;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static net.corda.core.crypto.Crypto.generateKeyPair;
import static net.corda.finance.Currencies.DOLLARS;
import static net.corda.finance.Currencies.issuedBy;
import static net.corda.finance.contracts.JavaCommercialPaper.JCP_PROGRAM_ID;
import static net.corda.testing.node.MockServicesKt.makeTestIdentityService;
import static net.corda.testing.core.TestConstants.*;
import static net.corda.testing.node.NodeTestUtils.ledger;
import static net.corda.testing.node.NodeTestUtils.transaction;
import static net.corda.testing.core.TestConstants.ALICE_NAME;
import static net.corda.testing.core.TestConstants.BOB_NAME;
import static net.corda.testing.core.TestConstants.TEST_TX_TIME;
public class CommercialPaperTest {
private static final TestIdentity ALICE = new TestIdentity(ALICE_NAME, 70L);
@ -32,7 +30,15 @@ public class CommercialPaperTest {
private static final TestIdentity BOB = new TestIdentity(BOB_NAME, 80L);
private static final TestIdentity MEGA_CORP = new TestIdentity(new CordaX500Name("MegaCorp", "London", "GB"));
private final byte[] defaultRef = {123};
private final MockServices ledgerServices = new MockServices(MEGA_CORP);
private MockServices ledgerServices;
@Before
public void setUp() {
// When creating the MockServices, you need to specify the packages that will contain the contracts you will use in this test
// For this test its' Cash and CommercialPaper, bot in the 'net.corda.finance.contracts' package
// In case you don't specify the 'cordappPackages' argument, the MockServices will be initialised with the Contracts found in the package of the current test
ledgerServices = new MockServices(singletonList("net.corda.finance.contracts"), MEGA_CORP);
}
// DOCSTART 1
private ICommercialPaperState getPaper() {

View File

@ -38,7 +38,9 @@ class CommercialPaperTest {
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule()
private val ledgerServices = MockServices(emptyList(), MEGA_CORP.name, rigorousMock<IdentityService>().also {
// When creating the MockServices, you need to specify the packages that will contain the contracts you will use in this test
// In case you don't specify the 'cordappPackages' argument, the MockServices will be initialised with the Contracts found in the package of the current test
private val ledgerServices = MockServices(listOf("net.corda.finance.contracts"), MEGA_CORP.name, rigorousMock<IdentityService>().also {
doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY)
doReturn(null).whenever(it).partyFromKey(BIG_CORP_PUBKEY)
doReturn(null).whenever(it).partyFromKey(ALICE_PUBKEY)

View File

@ -88,7 +88,7 @@ class ObligationTests {
doReturn(MINI_CORP).whenever(it).partyFromKey(MINI_CORP_PUBKEY)
}
private val mockService = MockServices(listOf("net.corda.finance.contracts.asset"), MEGA_CORP.name, identityService)
private val ledgerServices get() = MockServices(emptyList(), MEGA_CORP.name, identityService)
private val ledgerServices get() = MockServices(listOf("net.corda.finance.contracts.asset", "net.corda.testing.contracts"), MEGA_CORP.name, identityService)
private fun cashObligationTestRoots(
group: LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>
) = group.apply {

View File

@ -2,11 +2,9 @@ package net.corda.plugins
import groovy.lang.Closure
import net.corda.cordform.CordformDefinition
import org.apache.tools.ant.filters.FixCrLfFilter
import org.gradle.api.DefaultTask
import org.gradle.api.plugins.JavaPluginConvention
import org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME
import org.gradle.api.tasks.TaskAction
import java.io.File
import java.lang.reflect.InvocationTargetException
import java.net.URLClassLoader
@ -138,11 +136,13 @@ open class Baseform : DefaultTask() {
protected fun bootstrapNetwork() {
val networkBootstrapperClass = loadNetworkBootstrapperClass()
val networkBootstrapper = networkBootstrapperClass.newInstance()
val bootstrapMethod = networkBootstrapperClass.getMethod("bootstrap", Path::class.java).apply { isAccessible = true }
val bootstrapMethod = networkBootstrapperClass.getMethod("bootstrap", Path::class.java, List::class.java).apply { isAccessible = true }
// Call NetworkBootstrapper.bootstrap
try {
// Create a list of all cordapps used in this network and pass it to the bootstrapper.
val allCordapps = nodes.flatMap { it.additionalCordapps + it.getCordappList() }.distinct().map { it.absolutePath }
val rootDir = project.projectDir.toPath().resolve(directory).toAbsolutePath().normalize()
bootstrapMethod.invoke(networkBootstrapper, rootDir)
bootstrapMethod.invoke(networkBootstrapper, rootDir, allCordapps)
} catch (e: InvocationTargetException) {
throw e.cause!!
}

View File

@ -268,7 +268,7 @@ class Node(private val project: Project) : CordformNode() {
*
* @return List of this node's cordapps.
*/
private fun getCordappList(): Collection<File> {
fun getCordappList(): Collection<File> {
// Cordapps can sometimes contain a GString instance which fails the equality test with the Java string
@Suppress("RemoveRedundantCallsOfConversionMethods")
val cordapps: List<String> = cordapps.map { it.toString() }

View File

@ -34,6 +34,9 @@ dependencies {
// For AMQP serialisation.
compile "org.apache.qpid:proton-j:0.21.0"
// FastClasspathScanner: classpath scanning - needed for the NetworkBootstraper
compile 'io.github.lukehutch:fast-classpath-scanner:2.0.21'
// Unit testing helpers.
testCompile "junit:junit:$junit_version"
testCompile "org.assertj:assertj-core:$assertj_version"

View File

@ -1,7 +1,9 @@
package net.corda.nodeapi.internal
import net.corda.core.contracts.Attachment
import net.corda.core.contracts.ContractAttachment
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER
import net.corda.core.serialization.CordaSerializable
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
@ -31,6 +33,10 @@ class AttachmentsClassLoader(attachments: List<Attachment>, parent: ClassLoader
}
init {
require(attachments.mapNotNull { it as? ContractAttachment }.none { it.uploader != DEPLOYED_CORDAPP_UPLOADER }) {
"Attempting to load Contract Attachments downloaded from the network"
}
for (attachment in attachments) {
attachment.openAsJAR().use { jar ->
while (true) {

View File

@ -0,0 +1,41 @@
package net.corda.nodeapi.internal
import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner
import net.corda.core.contracts.Contract
import net.corda.core.contracts.ContractClassName
import net.corda.core.internal.copyTo
import net.corda.core.internal.deleteIfExists
import net.corda.core.internal.read
import java.io.File
import java.io.InputStream
import java.lang.reflect.Modifier
import java.net.URLClassLoader
import java.nio.file.Files
import java.nio.file.Paths
import java.nio.file.StandardCopyOption
/**
* Scans the jar for contracts.
* @returns: found contract class names or null if none found
*/
fun scanJarForContracts(cordappJarPath: String): List<ContractClassName> {
val currentClassLoader = Contract::class.java.classLoader
val scanResult = FastClasspathScanner().addClassLoader(currentClassLoader).overrideClasspath(cordappJarPath, Paths.get(Contract::class.java.protectionDomain.codeSource.location.toURI()).toString()).scan()
val contracts = (scanResult.getNamesOfClassesImplementing(Contract::class.qualifiedName) ).distinct()
// Only keep instantiable contracts
val classLoader = URLClassLoader(arrayOf(File(cordappJarPath).toURL()), currentClassLoader)
val concreteContracts = contracts.map(classLoader::loadClass).filter { !it.isInterface && !Modifier.isAbstract(it.modifiers) }
return concreteContracts.map { it.name }
}
fun <T> withContractsInJar(jarInputStream: InputStream, withContracts: (List<ContractClassName>, InputStream) -> T): T {
val tempFile = Files.createTempFile("attachment", ".jar")
try {
jarInputStream.copyTo(tempFile, StandardCopyOption.REPLACE_EXISTING)
val contracts = scanJarForContracts(tempFile.toAbsolutePath().toString())
return tempFile.read { withContracts(contracts, it) }
} finally {
tempFile.deleteIfExists()
}
}

View File

@ -1,13 +1,18 @@
package net.corda.nodeapi.internal.network
import com.google.common.hash.Hashing
import com.google.common.hash.HashingInputStream
import com.typesafe.config.ConfigFactory
import net.corda.cordform.CordformNode
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.SecureHash.Companion.parse
import net.corda.core.identity.Party
import net.corda.core.internal.*
import net.corda.core.internal.concurrent.fork
import net.corda.core.node.NetworkParameters
import net.corda.core.node.NodeInfo
import net.corda.core.node.NotaryInfo
import net.corda.core.node.services.AttachmentId
import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.internal.SerializationEnvironmentImpl
@ -16,11 +21,14 @@ import net.corda.core.utilities.ByteSequence
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.seconds
import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.scanJarForContracts
import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme
import net.corda.nodeapi.internal.serialization.kryo.AbstractKryoSerializationScheme
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
import java.io.File
import java.io.PrintStream
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
@ -44,15 +52,18 @@ class NetworkBootstrapper {
)
private const val LOGS_DIR_NAME = "logs"
private const val WHITELIST_FILE_NAME = "whitelist.txt"
@JvmStatic
fun main(args: Array<String>) {
val arg = args.singleOrNull() ?: throw IllegalArgumentException("Expecting single argument which is the nodes' parent directory")
NetworkBootstrapper().bootstrap(Paths.get(arg).toAbsolutePath().normalize())
val baseNodeDirectory = args.firstOrNull()
?: throw IllegalArgumentException("Expecting first argument which is the nodes' parent directory")
val cordapps = if (args.size > 1) args.toList().drop(1) else null
NetworkBootstrapper().bootstrap(Paths.get(baseNodeDirectory).toAbsolutePath().normalize(), cordapps)
}
}
fun bootstrap(directory: Path) {
fun bootstrap(directory: Path, cordapps: List<String>?) {
directory.createDirectories()
println("Bootstrapping local network in $directory")
generateDirectoriesIfNeeded(directory)
@ -69,7 +80,10 @@ class NetworkBootstrapper {
println("Gathering notary identities")
val notaryInfos = gatherNotaryInfos(nodeInfoFiles)
println("Notary identities to be used in network-parameters file: ${notaryInfos.joinToString("; ") { it.prettyPrint() }}")
installNetworkParameters(notaryInfos, nodeDirs)
val mergedWhiteList = generateWhitelist(directory / WHITELIST_FILE_NAME, cordapps)
println("Updating whitelist.")
overwriteWhitelist(directory / WHITELIST_FILE_NAME, mergedWhiteList)
installNetworkParameters(notaryInfos, nodeDirs, mergedWhiteList)
println("Bootstrapping complete!")
} finally {
_contextSerializationEnv.set(null)
@ -85,8 +99,7 @@ class NetworkBootstrapper {
for (confFile in confFiles) {
val nodeName = confFile.fileName.toString().removeSuffix(".conf")
println("Generating directory for $nodeName")
val nodeDir = (directory / nodeName)
if (!nodeDir.exists()) { nodeDir.createDirectory() }
val nodeDir = (directory / nodeName).createDirectories()
confFile.moveTo(nodeDir / "node.conf", StandardCopyOption.REPLACE_EXISTING)
Files.copy(cordaJar, (nodeDir / "corda.jar"), StandardCopyOption.REPLACE_EXISTING)
}
@ -158,7 +171,7 @@ class NetworkBootstrapper {
}.distinct() // We need distinct as nodes part of a distributed notary share the same notary identity
}
private fun installNetworkParameters(notaryInfos: List<NotaryInfo>, nodeDirs: List<Path>) {
private fun installNetworkParameters(notaryInfos: List<NotaryInfo>, nodeDirs: List<Path>, whitelist: Map<String, List<AttachmentId>>) {
// TODO Add config for minimumPlatformVersion, maxMessageSize and maxTransactionSize
val copier = NetworkParametersCopier(NetworkParameters(
minimumPlatformVersion = 1,
@ -166,12 +179,58 @@ class NetworkBootstrapper {
modifiedTime = Instant.now(),
maxMessageSize = 10485760,
maxTransactionSize = Int.MAX_VALUE,
epoch = 1
epoch = 1,
whitelistedContractImplementations = whitelist
), overwriteFile = true)
nodeDirs.forEach { copier.install(it) }
}
private fun generateWhitelist(whitelistFile: Path, cordapps: List<String>?): Map<String, List<AttachmentId>> {
val existingWhitelist = if (whitelistFile.exists()) readContractWhitelist(whitelistFile) else emptyMap()
println("Found existing whitelist: $existingWhitelist")
val newWhiteList = cordapps?.flatMap { cordappJarPath ->
val jarHash = getJarHash(cordappJarPath)
scanJarForContracts(cordappJarPath).map { contract ->
contract to jarHash
}
}?.toMap() ?: emptyMap()
println("Calculating whitelist for current cordapps: $newWhiteList")
val merged = (newWhiteList.keys + existingWhitelist.keys).map { contractClassName ->
val existing = existingWhitelist[contractClassName] ?: emptyList()
val newHash = newWhiteList[contractClassName]
contractClassName to (if (newHash == null || newHash in existing) existing else existing + newHash)
}.toMap()
println("Final whitelist: $merged")
return merged
}
private fun overwriteWhitelist(whitelistFile: Path, mergedWhiteList: Map<String, List<AttachmentId>>) {
PrintStream(whitelistFile.toFile().outputStream()).use { out ->
mergedWhiteList.forEach { (contract, attachments )->
out.println("${contract}:${attachments.joinToString(",")}")
}
}
}
private fun getJarHash(cordappPath: String): AttachmentId = File(cordappPath).inputStream().use { jar ->
val hs = HashingInputStream(Hashing.sha256(), jar)
hs.readBytes()
SecureHash.SHA256(hs.hash().asBytes())
}
private fun readContractWhitelist(file: Path): Map<String, List<AttachmentId>> = file.toFile().readLines()
.map { line -> line.split(":") }
.map { (contract, attachmentIds) ->
contract to (attachmentIds.split(",").map(::parse))
}.toMap()
private fun NotaryInfo.prettyPrint(): String = "${identity.name} (${if (validating) "" else "non-"}validating)"
private fun NodeInfo.notaryIdentity(): Party {

View File

@ -21,12 +21,12 @@ class ContractAttachmentSerializer(factory: SerializerFactory) : CustomSerialize
} catch (e: Exception) {
throw MissingAttachmentsException(listOf(obj.id))
}
return ContractAttachmentProxy(GeneratedAttachment(bytes), obj.contract)
return ContractAttachmentProxy(GeneratedAttachment(bytes), obj.contract, obj.additionalContracts, obj.uploader)
}
override fun fromProxy(proxy: ContractAttachmentProxy): ContractAttachment {
return ContractAttachment(proxy.attachment, proxy.contract)
return ContractAttachment(proxy.attachment, proxy.contract, proxy.contracts, proxy.uploader)
}
data class ContractAttachmentProxy(val attachment: Attachment, val contract: ContractClassName)
data class ContractAttachmentProxy(val attachment: Attachment, val contract: ContractClassName, val contracts: Set<ContractClassName>, val uploader: String?)
}

View File

@ -12,6 +12,7 @@ import de.javakaffee.kryoserializers.BitSetSerializer
import de.javakaffee.kryoserializers.UnmodifiableCollectionsSerializer
import de.javakaffee.kryoserializers.guava.*
import net.corda.core.contracts.ContractAttachment
import net.corda.core.contracts.ContractClassName
import net.corda.core.contracts.PrivacySalt
import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.SecureHash
@ -207,29 +208,34 @@ object DefaultKryoCustomizer {
output.writeBytesWithLength(buffer.toByteArray())
}
output.writeString(obj.contract)
kryo.writeClassAndObject(output, obj.additionalContracts)
output.writeString(obj.uploader)
}
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()
val additionalContracts = kryo.readClassAndObject(input) as Set<ContractClassName>
val uploader = input.readString()
val context = kryo.serializationContext()!!
val attachmentStorage = context.serviceHub.attachments
val lazyAttachment = object : AbstractAttachment({
val attachment = attachmentStorage.openAttachment(attachmentHash) ?: throw MissingAttachmentsException(listOf(attachmentHash))
val attachment = attachmentStorage.openAttachment(attachmentHash)
?: throw MissingAttachmentsException(listOf(attachmentHash))
attachment.open().readBytes()
}) {
override val id = attachmentHash
}
return ContractAttachment(lazyAttachment, contract)
return ContractAttachment(lazyAttachment, contract, additionalContracts, uploader)
} else {
val attachment = GeneratedAttachment(input.readBytesWithLength())
val contract = input.readString()
return ContractAttachment(attachment, contract)
val additionalContracts = kryo.readClassAndObject(input) as Set<ContractClassName>
val uploader = input.readString()
return ContractAttachment(attachment, contract, additionalContracts, uploader)
}
}
}

View File

@ -13,6 +13,7 @@ import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.node.internal.cordapp.CordappLoader
import net.corda.node.internal.cordapp.CordappProviderImpl
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
@ -58,7 +59,8 @@ class AttachmentsClassLoaderStaticContractTests {
}
private val serviceHub = rigorousMock<ServicesForResolution>().also {
doReturn(CordappProviderImpl(CordappLoader.createWithTestPackages(listOf("net.corda.nodeapi.internal")), MockAttachmentStorage())).whenever(it).cordappProvider
doReturn(CordappProviderImpl(CordappLoader.createWithTestPackages(listOf("net.corda.nodeapi.internal")), MockAttachmentStorage(), testNetworkParameters().whitelistedContractImplementations)).whenever(it).cordappProvider
doReturn(testNetworkParameters()).whenever(it).networkParameters
}
@Test

View File

@ -18,6 +18,7 @@ import net.corda.nodeapi.DummyContractBackdoor
import net.corda.nodeapi.internal.serialization.SerializeAsTokenContextImpl
import net.corda.nodeapi.internal.serialization.attachmentsClassLoaderEnabledPropertyName
import net.corda.nodeapi.internal.serialization.withTokenContext
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
@ -57,12 +58,15 @@ class AttachmentsClassLoaderTests {
@JvmField
val testSerialization = SerializationEnvironmentRule()
private val attachments = MockAttachmentStorage()
private val cordappProvider = CordappProviderImpl(CordappLoader.createDevMode(listOf(ISOLATED_CONTRACTS_JAR_PATH)), attachments)
private val networkParameters = testNetworkParameters()
private val cordappProvider = CordappProviderImpl(CordappLoader.createDevMode(listOf(ISOLATED_CONTRACTS_JAR_PATH)), attachments, networkParameters.whitelistedContractImplementations)
private val cordapp get() = cordappProvider.cordapps.first()
private val attachmentId get() = cordappProvider.getCordappAttachmentId(cordapp)!!
private val appContext get() = cordappProvider.getAppContext(cordapp)
private val serviceHub = rigorousMock<ServiceHub>().also {
doReturn(attachments).whenever(it).attachments
doReturn(cordappProvider).whenever(it).cordappProvider
doReturn(networkParameters).whenever(it).networkParameters
}
// These ClassLoaders work together to load 'AnotherDummyContract' in a disposable way, such that even though
@ -278,7 +282,7 @@ class AttachmentsClassLoaderTests {
.withClassLoader(child)
val bytes = run {
val wireTransaction = tx.toWireTransaction(cordappProvider, context)
val wireTransaction = tx.toWireTransaction(serviceHub, context)
wireTransaction.serialize(context = context)
}
val copiedWireTransaction = bytes.deserialize(context = context)
@ -302,7 +306,7 @@ class AttachmentsClassLoaderTests {
val outboundContext = SerializationFactory.defaultFactory.defaultContext
.withServiceHub(serviceHub)
.withClassLoader(child)
val wireTransaction = tx.toWireTransaction(cordappProvider, outboundContext)
val wireTransaction = tx.toWireTransaction(serviceHub, outboundContext)
wireTransaction.serialize(context = outboundContext)
}
// use empty attachmentStorage

View File

@ -42,6 +42,7 @@ class ContractAttachmentSerializerTest {
assertEquals(contractAttachment.id, deserialized.attachment.id)
assertEquals(contractAttachment.contract, deserialized.contract)
assertEquals(contractAttachment.additionalContracts, deserialized.additionalContracts)
assertArrayEquals(contractAttachment.open().readBytes(), deserialized.open().readBytes())
}
@ -57,6 +58,7 @@ class ContractAttachmentSerializerTest {
assertEquals(contractAttachment.id, deserialized.attachment.id)
assertEquals(contractAttachment.contract, deserialized.contract)
assertEquals(contractAttachment.additionalContracts, deserialized.additionalContracts)
assertArrayEquals(contractAttachment.open().readBytes(), deserialized.open().readBytes())
}

View File

@ -6,6 +6,7 @@ import com.esotericsoftware.kryo.io.Output
import com.esotericsoftware.kryo.util.DefaultClassResolver
import com.esotericsoftware.kryo.util.MapReferenceResolver
import com.nhaarman.mockito_kotlin.*
import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER
import net.corda.core.node.services.AttachmentStorage
import net.corda.core.serialization.*
import net.corda.nodeapi.internal.AttachmentsClassLoader
@ -196,7 +197,7 @@ class CordaClassResolverTests {
CordaClassResolver(emptyWhitelistContext).getRegistration(DefaultSerializable::class.java)
}
private fun importJar(storage: AttachmentStorage) = AttachmentsClassLoaderTests.ISOLATED_CONTRACTS_JAR_PATH.openStream().use { storage.importAttachment(it) }
private fun importJar(storage: AttachmentStorage, uploader: String = DEPLOYED_CORDAPP_UPLOADER) = AttachmentsClassLoaderTests.ISOLATED_CONTRACTS_JAR_PATH.openStream().use { storage.importAttachment(it, uploader, "") }
@Test(expected = KryoException::class)
fun `Annotation does not work in conjunction with AttachmentClassLoader annotation`() {
@ -207,6 +208,15 @@ class CordaClassResolverTests {
CordaClassResolver(emptyWhitelistContext).getRegistration(attachedClass)
}
@Test(expected = IllegalArgumentException::class)
fun `Attempt to load contract attachment with the incorrect uploader should fails with IAE`() {
val storage = MockAttachmentStorage()
val attachmentHash = importJar(storage, "some_uploader")
val classLoader = AttachmentsClassLoader(arrayOf(attachmentHash).map { storage.openAttachment(it)!! })
val attachedClass = Class.forName("net.corda.finance.contracts.isolated.AnotherDummyContract", true, classLoader)
CordaClassResolver(emptyWhitelistContext).getRegistration(attachedClass)
}
@Test
fun `Annotation is inherited from interfaces`() {
CordaClassResolver(emptyWhitelistContext).getRegistration(SerializableViaInterface::class.java)

View File

@ -523,7 +523,7 @@ class EvolvabilityTests {
val resource = "networkParams.<corda version>.<commit sha>"
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
val networkParameters = NetworkParameters(
3, listOf(NotaryInfo(DUMMY_NOTARY, false)),1000, 1000, Instant.EPOCH, 1 )
3, listOf(NotaryInfo(DUMMY_NOTARY, false)),1000, 1000, Instant.EPOCH, 1 , emptyMap())
val sf = testDefaultFactory()
sf.register(net.corda.nodeapi.internal.serialization.amqp.custom.InstantSerializer(sf))

View File

@ -20,8 +20,6 @@ import net.corda.nodeapi.internal.serialization.AllWhitelist
import net.corda.nodeapi.internal.serialization.EmptyWhitelist
import net.corda.nodeapi.internal.serialization.GeneratedAttachment
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory.Companion.isPrimitive
import net.corda.nodeapi.internal.serialization.amqp.custom.BigDecimalSerializer
import net.corda.nodeapi.internal.serialization.amqp.custom.CurrencySerializer
import net.corda.testing.contracts.DummyContract
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.SerializationEnvironmentRule
@ -38,13 +36,11 @@ import org.junit.Test
import java.io.ByteArrayInputStream
import java.io.IOException
import java.io.NotSerializableException
import java.lang.reflect.Type
import java.math.BigDecimal
import java.nio.ByteBuffer
import java.time.*
import java.time.temporal.ChronoUnit
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import kotlin.reflect.full.superclasses
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
@ -1065,6 +1061,7 @@ class SerializationOutputTests {
val obj2 = serdes(obj, factory, factory2, expectedEqual = false, expectDeserializedEqual = false)
assertEquals(obj.id, obj2.attachment.id)
assertEquals(obj.contract, obj2.contract)
assertEquals(obj.additionalContracts, obj2.additionalContracts)
assertArrayEquals(obj.open().readBytes(), obj2.open().readBytes())
}

View File

@ -159,9 +159,6 @@ dependencies {
compile 'commons-codec:commons-codec:1.10'
compile 'com.github.bft-smart:library:master-v1.1-beta-g6215ec8-87'
// FastClasspathScanner: classpath scanning
compile 'io.github.lukehutch:fast-classpath-scanner:2.0.21'
// Apache Shiro: authentication, authorization and session management.
compile "org.apache.shiro:shiro-core:${shiro_version}"

View File

@ -48,7 +48,7 @@ class AttachmentLoadingTests {
@JvmField
val testSerialization = SerializationEnvironmentRule()
private val attachments = MockAttachmentStorage()
private val provider = CordappProviderImpl(CordappLoader.createDevMode(listOf(isolatedJAR)), attachments)
private val provider = CordappProviderImpl(CordappLoader.createDevMode(listOf(isolatedJAR)), attachments, testNetworkParameters().whitelistedContractImplementations)
private val cordapp get() = provider.cordapps.first()
private val attachmentId get() = provider.getCordappAttachmentId(cordapp)!!
private val appContext get() = provider.getAppContext(cordapp)

View File

@ -13,7 +13,6 @@ import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.internal.deleteIfExists
import net.corda.core.internal.div
import net.corda.core.node.NotaryInfo
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.NetworkHostAndPort
@ -26,9 +25,10 @@ import net.corda.node.services.transactions.minClusterSize
import net.corda.node.services.transactions.minCorrectReplicas
import net.corda.nodeapi.internal.DevIdentityGenerator
import net.corda.nodeapi.internal.network.NetworkParametersCopier
import net.corda.core.node.NotaryInfo
import net.corda.testing.core.chooseIdentity
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.contracts.DummyContract
import net.corda.testing.core.chooseIdentity
import net.corda.testing.core.dummyCommand
import net.corda.testing.node.MockNodeParameters
import net.corda.testing.node.internal.InternalMockNetwork
@ -48,7 +48,7 @@ class BFTNotaryServiceTests {
@Before
fun before() {
mockNet = InternalMockNetwork(emptyList())
mockNet = InternalMockNetwork(listOf("net.corda.testing.contracts"))
}
@After

View File

@ -552,7 +552,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
checkpointStorage = DBCheckpointStorage()
val metrics = MetricRegistry()
attachments = NodeAttachmentService(metrics, configuration.attachmentContentCacheSizeBytes, configuration.attachmentCacheBound)
val cordappProvider = CordappProviderImpl(cordappLoader, attachments)
val cordappProvider = CordappProviderImpl(cordappLoader, attachments, networkParameters.whitelistedContractImplementations)
val keyManagementService = makeKeyManagementService(identityService, keyPairs)
_services = ServiceHubInternalImpl(
identityService,

View File

@ -13,6 +13,7 @@ import net.corda.core.identity.AbstractParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.internal.FlowStateMachine
import net.corda.core.internal.RPC_UPLOADER
import net.corda.core.internal.sign
import net.corda.core.messaging.*
import net.corda.core.node.NodeInfo
@ -189,7 +190,7 @@ internal class CordaRPCOpsImpl(
override fun uploadAttachment(jar: InputStream): SecureHash {
// TODO: this operation should not require an explicit transaction
return database.transaction {
services.attachments.importAttachment(jar)
services.attachments.importAttachment(jar, RPC_UPLOADER, null)
}
}

View File

@ -1,10 +1,12 @@
package net.corda.node.internal.cordapp
import com.google.common.collect.HashBiMap
import net.corda.core.contracts.ContractAttachment
import net.corda.core.contracts.ContractClassName
import net.corda.core.cordapp.Cordapp
import net.corda.core.cordapp.CordappContext
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER
import net.corda.core.node.services.AttachmentId
import net.corda.core.node.services.AttachmentStorage
import net.corda.core.serialization.SingletonSerializeAsToken
@ -14,12 +16,43 @@ import java.net.URL
/**
* Cordapp provider and store. For querying CorDapps for their attachment and vice versa.
*/
open class CordappProviderImpl(private val cordappLoader: CordappLoader, attachmentStorage: AttachmentStorage) : SingletonSerializeAsToken(), CordappProviderInternal {
open class CordappProviderImpl(private val cordappLoader: CordappLoader,
attachmentStorage: AttachmentStorage,
private val whitelistedContractImplementations: Map<String, List<AttachmentId>>) : SingletonSerializeAsToken(), CordappProviderInternal {
companion object {
private val log = loggerFor<CordappProviderImpl>()
}
/**
* Current known CorDapps loaded on this node
*/
override val cordapps get() = cordappLoader.cordapps
private val cordappAttachments = HashBiMap.create(loadContractsIntoAttachmentStore(attachmentStorage))
init {
verifyInstalledCordapps(attachmentStorage)
}
private fun verifyInstalledCordapps(attachmentStorage: AttachmentStorage) {
if (whitelistedContractImplementations.isEmpty()) {
log.warn("The network parameters don't specify any whitelisted contract implementations. Please contact your zone operator. See https://docs.corda.net/network-map.html")
return
}
// Verify that the installed contract classes correspond with the whitelist hash
// And warn if node is not using latest CorDapp
cordappAttachments.keys.map(attachmentStorage::openAttachment).mapNotNull { it as? ContractAttachment }.forEach { attch ->
(attch.allContracts intersect whitelistedContractImplementations.keys).forEach { contractClassName ->
when {
attch.id !in whitelistedContractImplementations[contractClassName]!! -> log.error("Contract $contractClassName found in attachment ${attch.id} is not whitelisted in the network parameters. If this is a production node contact your zone operator. See https://docs.corda.net/network-map.html")
attch.id != whitelistedContractImplementations[contractClassName]!!.last() -> log.warn("You are not using the latest CorDapp version for contract: $contractClassName. Please contact your zone operator.")
}
}
}
}
override fun getAppContext(): CordappContext {
// TODO: Use better supported APIs in Java 9
Exception().stackTrace.forEach { stackFrame ->
@ -36,11 +69,6 @@ open class CordappProviderImpl(private val cordappLoader: CordappLoader, attachm
return getCordappForClass(contractClassName)?.let(this::getCordappAttachmentId)
}
/**
* Current known CorDapps loaded on this node
*/
override val cordapps get() = cordappLoader.cordapps
private val cordappAttachments = HashBiMap.create(loadContractsIntoAttachmentStore(attachmentStorage))
/**
* Gets the attachment ID of this CorDapp. Only CorDapps with contracts have an attachment ID
*
@ -49,11 +77,16 @@ open class CordappProviderImpl(private val cordappLoader: CordappLoader, attachm
*/
fun getCordappAttachmentId(cordapp: Cordapp): SecureHash? = cordappAttachments.inverse().get(cordapp.jarPath)
private fun loadContractsIntoAttachmentStore(attachmentStorage: AttachmentStorage): Map<SecureHash, URL> {
val cordappsWithAttachments = cordapps.filter { !it.contractClassNames.isEmpty() }.map { it.jarPath }
val attachmentIds = cordappsWithAttachments.map { it.openStream().use { attachmentStorage.importOrGetAttachment(it) }}
return attachmentIds.zip(cordappsWithAttachments).toMap()
}
private fun loadContractsIntoAttachmentStore(attachmentStorage: AttachmentStorage): Map<SecureHash, URL> =
cordapps.filter { !it.contractClassNames.isEmpty() }.map {
it.jarPath.openStream().use { stream ->
try {
attachmentStorage.importAttachment(stream, DEPLOYED_CORDAPP_UPLOADER, null)
} catch (faee: java.nio.file.FileAlreadyExistsException) {
AttachmentId.parse(faee.message!!)
}
} to it.jarPath
}.toMap()
/**
* Get the current cordapp context for the given CorDapp

View File

@ -8,8 +8,11 @@ import com.google.common.hash.HashingInputStream
import com.google.common.io.CountingInputStream
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.internal.AbstractAttachment
import net.corda.core.internal.UNKNOWN_UPLOADER
import net.corda.core.internal.VisibleForTesting
import net.corda.core.node.services.AttachmentId
import net.corda.core.node.services.AttachmentStorage
@ -24,6 +27,7 @@ import net.corda.node.utilities.NonInvalidatingWeightBasedCache
import net.corda.node.utilities.defaultCordaCacheConcurrencyLevel
import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX
import net.corda.nodeapi.internal.persistence.currentDBSession
import net.corda.nodeapi.internal.withContractsInJar
import java.io.*
import java.nio.file.Paths
import java.time.Instant
@ -85,7 +89,14 @@ class NodeAttachmentService(
var uploader: String? = null,
@Column(name = "filename", updatable = false)
var filename: String? = null
var filename: String? = null,
@ElementCollection
@Column(name = "contract_class_name")
@CollectionTable(name = "node_attachments_contract_class_name", joinColumns = arrayOf(
JoinColumn(name = "att_id", referencedColumnName = "att_id")),
foreignKey = ForeignKey(name = "FK__ctr_class__attachments"))
var contractClassNames: List<ContractClassName>? = null
) : Serializable
@VisibleForTesting
@ -196,23 +207,31 @@ class NodeAttachmentService(
// If repeatedly looking for non-existing attachments becomes a performance issue, this is either indicating a
// a problem somewhere else or this needs to be revisited.
private val attachmentContentCache = NonInvalidatingWeightBasedCache<SecureHash, Optional<ByteArray>>(
private val attachmentContentCache = NonInvalidatingWeightBasedCache<SecureHash, Optional<Pair<Attachment, ByteArray>>>(
maxWeight = attachmentContentCacheSize,
concurrencyLevel = defaultCordaCacheConcurrencyLevel,
weigher = object : Weigher<SecureHash, Optional<ByteArray>> {
override fun weigh(key: SecureHash, value: Optional<ByteArray>): Int {
return key.size + if (value.isPresent) value.get().size else 0
weigher = object : Weigher<SecureHash, Optional<Pair<Attachment, ByteArray>>> {
override fun weigh(key: SecureHash, value: Optional<Pair<Attachment, ByteArray>>): Int {
return key.size + if (value.isPresent) value.get().second.size else 0
}
},
loadFunction = { Optional.ofNullable(loadAttachmentContent(it)) }
)
private fun loadAttachmentContent(id: SecureHash): ByteArray? {
private fun loadAttachmentContent(id: SecureHash): Pair<Attachment, ByteArray>? {
val attachment = currentDBSession().get(NodeAttachmentService.DBAttachment::class.java, id.toString())
return attachment?.content
?: return 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)
} else {
it
}
}
return Pair(attachmentImpl, attachment.content)
}
private val attachmentCache = NonInvalidatingCache<SecureHash, Optional<Attachment>>(
attachmentCacheBound,
defaultCordaCacheConcurrencyLevel,
@ -222,16 +241,7 @@ class NodeAttachmentService(
private fun createAttachment(key: SecureHash): Attachment? {
val content = attachmentContentCache.get(key)
if (content.isPresent) {
return AttachmentImpl(
key,
{
attachmentContentCache
.get(key)
.orElseThrow {
IllegalArgumentException("No attachement impl should have been created for non existent content")
}
},
checkAttachmentsOnLoad)
return content.get().first
}
// if no attachement has been found, we don't want to cache that - it might arrive later
attachmentContentCache.invalidate(key)
@ -248,10 +258,10 @@ class NodeAttachmentService(
}
override fun importAttachment(jar: InputStream): AttachmentId {
return import(jar, null, null)
return import(jar, UNKNOWN_UPLOADER, null)
}
override fun importAttachment(jar: InputStream, uploader: String, filename: String): AttachmentId {
override fun importAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId {
return import(jar, uploader, filename)
}
@ -263,47 +273,39 @@ class NodeAttachmentService(
return Pair(id, bytes)
}
override fun hasAttachment(attachmentId: AttachmentId): Boolean {
val session = currentDBSession()
val criteriaBuilder = session.criteriaBuilder
val criteriaQuery = criteriaBuilder.createQuery(Long::class.java)
val attachments = criteriaQuery.from(NodeAttachmentService.DBAttachment::class.java)
criteriaQuery.select(criteriaBuilder.count(criteriaQuery.from(NodeAttachmentService.DBAttachment::class.java)))
criteriaQuery.where(criteriaBuilder.equal(attachments.get<String>(DBAttachment::attId.name), attachmentId.toString()))
return (session.createQuery(criteriaQuery).singleResult > 0)
}
override fun hasAttachment(attachmentId: AttachmentId): Boolean =
currentDBSession().find(NodeAttachmentService.DBAttachment::class.java, attachmentId.toString()) != null
// TODO: PLT-147: The attachment should be randomised to prevent brute force guessing and thus privacy leaks.
private fun import(jar: InputStream, uploader: String?, filename: String?): AttachmentId {
require(jar !is JarInputStream)
return withContractsInJar(jar) { contractClassNames, inputStream ->
require(inputStream !is JarInputStream)
// Read the file into RAM, hashing it to find the ID as we go. The attachment must fit into memory.
// TODO: Switch to a two-phase insert so we can handle attachments larger than RAM.
// To do this we must pipe stream into the database without knowing its hash, which we will learn only once
// the insert/upload is complete. We can then query to see if it's a duplicate and if so, erase, and if not
// set the hash field of the new attachment record.
// Read the file into RAM, hashing it to find the ID as we go. The attachment must fit into memory.
// TODO: Switch to a two-phase insert so we can handle attachments larger than RAM.
// To do this we must pipe stream into the database without knowing its hash, which we will learn only once
// the insert/upload is complete. We can then query to see if it's a duplicate and if so, erase, and if not
// set the hash field of the new attachment record.
val (id, bytes) = getAttachmentIdAndBytes(jar)
if (!hasAttachment(id)) {
checkIsAValidJAR(ByteArrayInputStream(bytes))
val session = currentDBSession()
val attachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = bytes, uploader = uploader, filename = filename)
session.save(attachment)
attachmentCount.inc()
log.info("Stored new attachment $id")
return id
} else {
throw java.nio.file.FileAlreadyExistsException(id.toString())
val (id, bytes) = getAttachmentIdAndBytes(inputStream)
if (!hasAttachment(id)) {
checkIsAValidJAR(ByteArrayInputStream(bytes))
val session = currentDBSession()
val attachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = bytes, uploader = uploader, filename = filename, contractClassNames = contractClassNames)
session.save(attachment)
attachmentCount.inc()
log.info("Stored new attachment $id")
id
} else {
throw java.nio.file.FileAlreadyExistsException(id.toString())
}
}
}
override fun importOrGetAttachment(jar: InputStream): AttachmentId {
try {
return importAttachment(jar)
}
catch (faee: java.nio.file.FileAlreadyExistsException) {
return AttachmentId.parse(faee.message!!)
}
override fun importOrGetAttachment(jar: InputStream): AttachmentId = try {
importAttachment(jar)
} catch (faee: java.nio.file.FileAlreadyExistsException) {
AttachmentId.parse(faee.message!!)
}
override fun queryAttachments(criteria: AttachmentQueryCriteria, sorting: AttachmentSort?): List<AttachmentId> {
@ -328,5 +330,4 @@ class NodeAttachmentService(
return results.map { AttachmentId.parse(it.attId) }
}
}

View File

@ -1,10 +1,12 @@
package net.corda.node.internal.cordapp
import net.corda.core.node.services.AttachmentStorage
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.services.MockAttachmentStorage
import org.junit.Assert
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import java.net.URL
class CordappProviderImplTests {
companion object {
@ -13,6 +15,7 @@ class CordappProviderImplTests {
}
private lateinit var attachmentStore: AttachmentStorage
private val whitelistedContractImplementations = testNetworkParameters().whitelistedContractImplementations
@Before
fun setup() {
@ -21,43 +24,44 @@ class CordappProviderImplTests {
@Test
fun `isolated jar is loaded into the attachment store`() {
val loader = CordappLoader.createDevMode(listOf(isolatedJAR))
val provider = CordappProviderImpl(loader, attachmentStore)
val provider = newCordappProvider(isolatedJAR)
val maybeAttachmentId = provider.getCordappAttachmentId(provider.cordapps.first())
Assert.assertNotNull(maybeAttachmentId)
Assert.assertNotNull(attachmentStore.openAttachment(maybeAttachmentId!!))
assertNotNull(maybeAttachmentId)
assertNotNull(attachmentStore.openAttachment(maybeAttachmentId!!))
}
@Test
fun `empty jar is not loaded into the attachment store`() {
val loader = CordappLoader.createDevMode(listOf(emptyJAR))
val provider = CordappProviderImpl(loader, attachmentStore)
Assert.assertNull(provider.getCordappAttachmentId(provider.cordapps.first()))
val provider = newCordappProvider(emptyJAR)
assertNull(provider.getCordappAttachmentId(provider.cordapps.first()))
}
@Test
fun `test that we find a cordapp class that is loaded into the store`() {
val loader = CordappLoader.createDevMode(listOf(isolatedJAR))
val provider = CordappProviderImpl(loader, attachmentStore)
val provider = newCordappProvider(isolatedJAR)
val className = "net.corda.finance.contracts.isolated.AnotherDummyContract"
val expected = provider.cordapps.first()
val actual = provider.getCordappForClass(className)
Assert.assertNotNull(actual)
Assert.assertEquals(expected, actual)
assertNotNull(actual)
assertEquals(expected, actual)
}
@Test
fun `test that we find an attachment for a cordapp contrat class`() {
val loader = CordappLoader.createDevMode(listOf(isolatedJAR))
val provider = CordappProviderImpl(loader, attachmentStore)
val provider = newCordappProvider(isolatedJAR)
val className = "net.corda.finance.contracts.isolated.AnotherDummyContract"
val expected = provider.getAppContext(provider.cordapps.first()).attachmentId
val actual = provider.getContractAttachmentID(className)
Assert.assertNotNull(actual)
Assert.assertEquals(actual!!, expected)
assertNotNull(actual)
assertEquals(actual!!, expected)
}
private fun newCordappProvider(vararg urls: URL): CordappProviderImpl {
val loader = CordappLoader.createDevMode(urls.toList())
return CordappProviderImpl(loader, attachmentStore, whitelistedContractImplementations)
}
}

View File

@ -226,7 +226,7 @@ open class MockServices private constructor(
return NodeInfo(listOf(NetworkHostAndPort("mock.node.services", 10000)), listOf(initialIdentity.identity), 1, serial = 1L)
}
override val transactionVerifierService: TransactionVerifierService get() = InMemoryTransactionVerifierService(2)
private val mockCordappProvider: MockCordappProvider = MockCordappProvider(cordappLoader, attachments)
private val mockCordappProvider: MockCordappProvider = MockCordappProvider(cordappLoader, attachments, networkParameters.whitelistedContractImplementations)
override val cordappProvider: CordappProvider get() = mockCordappProvider
internal fun makeVaultService(hibernateConfig: HibernateConfiguration, schemaService: SchemaService): VaultServiceInternal {

View File

@ -39,7 +39,7 @@ class NetworkMapServer(private val cacheTimeout: Duration,
private val myHostNameValue: String = "test.host.name",
vararg additionalServices: Any) : Closeable {
companion object {
private val stubNetworkParameters = NetworkParameters(1, emptyList(), 10485760, Int.MAX_VALUE, Instant.now(), 10)
private val stubNetworkParameters = NetworkParameters(1, emptyList(), 10485760, Int.MAX_VALUE, Instant.now(), 10, emptyMap())
}
private val server: Server

View File

@ -19,6 +19,7 @@ fun testNetworkParameters(
modifiedTime = modifiedTime,
maxMessageSize = maxMessageSize,
maxTransactionSize = maxTransactionSize,
epoch = epoch
epoch = epoch,
whitelistedContractImplementations = emptyMap()
)
}

View File

@ -2,6 +2,7 @@ package net.corda.testing.internal
import net.corda.core.contracts.ContractClassName
import net.corda.core.cordapp.Cordapp
import net.corda.core.internal.TEST_UPLOADER
import net.corda.core.internal.cordapp.CordappImpl
import net.corda.core.node.services.AttachmentId
import net.corda.core.node.services.AttachmentStorage
@ -11,7 +12,9 @@ import net.corda.testing.services.MockAttachmentStorage
import java.nio.file.Paths
import java.util.*
class MockCordappProvider(cordappLoader: CordappLoader, attachmentStorage: AttachmentStorage) : CordappProviderImpl(cordappLoader, attachmentStorage) {
class MockCordappProvider(cordappLoader: CordappLoader,
attachmentStorage: AttachmentStorage,
whitelistedContractImplementations: Map<String, List<AttachmentId>>) : CordappProviderImpl(cordappLoader, attachmentStorage, whitelistedContractImplementations) {
val cordappRegistry = mutableListOf<Pair<Cordapp, AttachmentId>>()
fun addMockCordapp(contractClassName: ContractClassName, attachments: MockAttachmentStorage) {
@ -27,20 +30,21 @@ class MockCordappProvider(cordappLoader: CordappLoader, attachmentStorage: Attac
customSchemas = emptySet(),
jarPath = Paths.get("").toUri().toURL())
if (cordappRegistry.none { it.first.contractClassNames.contains(contractClassName) }) {
cordappRegistry.add(Pair(cordapp, findOrImportAttachment(contractClassName.toByteArray(), attachments)))
cordappRegistry.add(Pair(cordapp, findOrImportAttachment(listOf(contractClassName), contractClassName.toByteArray(), attachments)))
}
}
override fun getContractAttachmentID(contractClassName: ContractClassName): AttachmentId? = 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(data: ByteArray, attachments: MockAttachmentStorage): AttachmentId {
private fun findOrImportAttachment(contractClassNames: List<ContractClassName>, data: ByteArray, attachments: MockAttachmentStorage): AttachmentId {
val existingAttachment = attachments.files.filter {
Arrays.equals(it.value, data)
Arrays.equals(it.value.second, data)
}
return if (!existingAttachment.isEmpty()) {
existingAttachment.keys.first()
} else {
attachments.importAttachment(data.inputStream())
attachments.importContractAttachment(contractClassNames, TEST_UPLOADER, data.inputStream())
}
}
}

View File

@ -1,14 +1,18 @@
package net.corda.testing.services
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.sha256
import net.corda.core.internal.AbstractAttachment
import net.corda.core.internal.UNKNOWN_UPLOADER
import net.corda.core.node.services.AttachmentId
import net.corda.core.node.services.AttachmentStorage
import net.corda.core.node.services.vault.AttachmentQueryCriteria
import net.corda.core.node.services.vault.AttachmentSort
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.nodeapi.internal.withContractsInJar
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.util.*
@ -24,31 +28,17 @@ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() {
}
}
override fun importAttachment(jar: InputStream): 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 files = HashMap<SecureHash, Pair<Attachment, ByteArray>>()
val bytes = getBytes(jar)
override fun importAttachment(jar: InputStream): AttachmentId = importAttachment(jar, UNKNOWN_UPLOADER, null)
val sha256 = bytes.sha256()
if (!files.containsKey(sha256)) {
files[sha256] = bytes
override fun importAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId {
return withContractsInJar(jar) { contractClassNames, inputStream ->
importAttachmentInternal(inputStream, uploader, filename, contractClassNames)
}
return sha256
}
override fun importAttachment(jar: InputStream, uploader: String, filename: String): AttachmentId {
return importAttachment(jar)
}
val files = HashMap<SecureHash, ByteArray>()
private class MockAttachment(dataLoader: () -> ByteArray, override val id: SecureHash): AbstractAttachment(dataLoader)
override fun openAttachment(id: SecureHash): Attachment? {
val f = files[id] ?: return null
return MockAttachment({f}, id)
}
override fun openAttachment(id: SecureHash): Attachment? = files[id]?.first
override fun queryAttachments(criteria: AttachmentQueryCriteria, sorting: AttachmentSort?): List<AttachmentId> {
throw NotImplementedError("Querying for attachments not implemented")
@ -56,18 +46,33 @@ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() {
override fun hasAttachment(attachmentId: AttachmentId) = files.containsKey(attachmentId)
fun getAttachmentIdAndBytes(jar: InputStream): Pair<AttachmentId, ByteArray> {
val bytes = getBytes(jar)
return Pair(bytes.sha256(), bytes)
}
override fun importOrGetAttachment(jar: InputStream): AttachmentId {
try {
return importAttachment(jar)
}
catch (faee: java.nio.file.FileAlreadyExistsException) {
} catch (faee: java.nio.file.FileAlreadyExistsException) {
return AttachmentId.parse(faee.message!!)
}
}
fun importContractAttachment(contractClassNames: List<ContractClassName>, uploader: String, jar: InputStream): AttachmentId = importAttachmentInternal(jar, uploader, null, contractClassNames)
fun getAttachmentIdAndBytes(jar: InputStream): Pair<AttachmentId, ByteArray> = getBytes(jar).let { bytes -> Pair(bytes.sha256(), bytes) }
private class MockAttachment(dataLoader: () -> ByteArray, override val id: SecureHash): AbstractAttachment(dataLoader)
private fun importAttachmentInternal(jar: InputStream, uploader: String, filename: String?, contractClassNames: List<ContractClassName>? = null): 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 = getBytes(jar)
val sha256 = 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)
files[sha256] = Pair(attachment, bytes)
}
return sha256
}
}

View File

@ -145,7 +145,8 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() {
modifiedTime = Instant.now(),
maxMessageSize = 10485760,
maxTransactionSize = Int.MAX_VALUE,
epoch = 1
epoch = 1,
whitelistedContractImplementations = emptyMap()
))
notaryIdentity = identity
networkParametersCopier = parametersCopier

View File

@ -41,7 +41,7 @@ data class GeneratedLedger(
private val attachmentMap: Map<SecureHash, Attachment> by lazy { attachments.associateBy(Attachment::id) }
private val identityMap: Map<PublicKey, Party> by lazy { identities.associateBy(Party::owningKey) }
private val contractAttachmentMap: Map<String, ContractAttachment> by lazy {
attachments.mapNotNull { it as? ContractAttachment }.associateBy { it.contract }
attachments.mapNotNull { it as? ContractAttachment }.flatMap { attch-> attch.allContracts.map { it to attch } }.toMap()
}
private val services = object : ServicesForResolution {

View File

@ -17,6 +17,7 @@ import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.driver.internal.NodeHandleInternal
import net.corda.testing.node.NotarySpec
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import java.util.*
@ -133,6 +134,7 @@ class VerifierTests {
}
}
@Ignore("CORDA-1022")
@Test
fun `single verifier works with a node`() {
verifierDriver(