diff --git a/.ci/api-current.txt b/.ci/api-current.txt index b5cf7b32e1..6954e313f5 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -14,8 +14,6 @@ public void setMessage(String) public void setOriginalExceptionClassName(String) ## -public @interface net.corda.core.CordaInternal -## public final class net.corda.core.CordaOID extends java.lang.Object @org.jetbrains.annotations.NotNull public static final String CORDA_PLATFORM = "1.3.6.1.4.1.50530.1" public static final net.corda.core.CordaOID INSTANCE @@ -209,7 +207,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 +282,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 (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 ## @@ -343,14 +341,20 @@ public final class net.corda.core.contracts.ComponentGroupEnum extends java.lang ## @net.corda.core.serialization.CordaSerializable public final class net.corda.core.contracts.ContractAttachment extends java.lang.Object implements net.corda.core.contracts.Attachment public (net.corda.core.contracts.Attachment, String) + public (net.corda.core.contracts.Attachment, String, Set) + public (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 +370,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 (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 +561,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 (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 (net.corda.core.crypto.SecureHash, String) ## @@ -620,6 +627,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 boolean isSatisfiedBy(net.corda.core.contracts.Attachment) + public static final net.corda.core.contracts.WhitelistedByZoneAttachmentConstraint INSTANCE +## @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() @@ -634,10 +645,25 @@ public static final class net.corda.core.contracts.UniqueIdentifier$Companion ex @org.jetbrains.annotations.NotNull public abstract List getServiceFlows() @org.jetbrains.annotations.NotNull public abstract List getServices() ## +@net.corda.core.DoNotImplement public interface net.corda.core.cordapp.CordappConfig + public abstract boolean exists(String) + @org.jetbrains.annotations.NotNull public abstract Object get(String) + public abstract boolean getBoolean(String) + public abstract double getDouble(String) + public abstract float getFloat(String) + public abstract int getInt(String) + public abstract long getLong(String) + @org.jetbrains.annotations.NotNull public abstract Number getNumber(String) + @org.jetbrains.annotations.NotNull public abstract String getString(String) +## +public final class net.corda.core.cordapp.CordappConfigException extends java.lang.Exception + public (String, Throwable) +## public final class net.corda.core.cordapp.CordappContext extends java.lang.Object - public (net.corda.core.cordapp.Cordapp, net.corda.core.crypto.SecureHash, ClassLoader) + public (net.corda.core.cordapp.Cordapp, net.corda.core.crypto.SecureHash, ClassLoader, net.corda.core.cordapp.CordappConfig) @org.jetbrains.annotations.Nullable public final net.corda.core.crypto.SecureHash getAttachmentId() @org.jetbrains.annotations.NotNull public final ClassLoader getClassLoader() + @org.jetbrains.annotations.NotNull public final net.corda.core.cordapp.CordappConfig getConfig() @org.jetbrains.annotations.NotNull public final net.corda.core.cordapp.Cordapp getCordapp() ## @net.corda.core.DoNotImplement public interface net.corda.core.cordapp.CordappProvider @@ -1128,7 +1154,8 @@ public final class net.corda.core.flows.ContractUpgradeFlow extends java.lang.Ob public (net.corda.core.contracts.StateAndRef, Class) @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull protected net.corda.core.flows.AbstractStateReplacementFlow$UpgradeTx assembleTx() ## -public abstract class net.corda.core.flows.DataVendingFlow extends net.corda.core.flows.FlowLogic +public class net.corda.core.flows.DataVendingFlow extends net.corda.core.flows.FlowLogic + public (net.corda.core.flows.FlowSession, Object) @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.Nullable public Void call() @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowSession getOtherSideSession() @org.jetbrains.annotations.NotNull public final Object getPayload() @@ -1233,16 +1260,14 @@ public abstract class net.corda.core.flows.FlowLogic extends java.lang.Object @org.jetbrains.annotations.Nullable public net.corda.core.utilities.ProgressTracker getProgressTracker() @org.jetbrains.annotations.NotNull public final net.corda.core.flows.StateMachineRunId getRunId() @org.jetbrains.annotations.NotNull public final net.corda.core.node.ServiceHub getServiceHub() - @net.corda.core.CordaInternal @org.jetbrains.annotations.NotNull public final net.corda.core.internal.FlowStateMachine getStateMachine() @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowSession initiateFlow(net.corda.core.identity.Party) @co.paralleluniverse.fibers.Suspendable public final void persistFlowStackSnapshot() @kotlin.Deprecated @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public net.corda.core.utilities.UntrustworthyData receive(Class, net.corda.core.identity.Party) @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public List receiveAll(Class, List) - @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public Map receiveAll(Map) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public Map receiveAllMap(Map) public final void recordAuditEvent(String, String, Map) @kotlin.Deprecated @co.paralleluniverse.fibers.Suspendable public void send(net.corda.core.identity.Party, Object) @kotlin.Deprecated @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public net.corda.core.utilities.UntrustworthyData sendAndReceive(Class, net.corda.core.identity.Party, Object) - @net.corda.core.CordaInternal public final void setStateMachine(net.corda.core.internal.FlowStateMachine) @co.paralleluniverse.fibers.Suspendable @kotlin.jvm.JvmStatic public static final void sleep(java.time.Duration) @co.paralleluniverse.fibers.Suspendable public Object subFlow(net.corda.core.flows.FlowLogic) @org.jetbrains.annotations.Nullable public final net.corda.core.messaging.DataFeed track() @@ -1306,6 +1331,39 @@ public @interface net.corda.core.flows.InitiatedBy public @interface net.corda.core.flows.InitiatingFlow public abstract int version() ## +@net.corda.core.serialization.CordaSerializable public final class net.corda.core.flows.NotarisationPayload extends java.lang.Object + public (Object, net.corda.core.flows.NotarisationRequestSignature) + @org.jetbrains.annotations.NotNull public final Object component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.NotarisationRequestSignature component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.NotarisationPayload copy(Object, net.corda.core.flows.NotarisationRequestSignature) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.CoreTransaction getCoreTransaction() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.NotarisationRequestSignature getRequestSignature() + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction getSignedTransaction() + @org.jetbrains.annotations.NotNull public final Object getTransaction() + public int hashCode() + public String toString() +## +@net.corda.core.serialization.CordaSerializable public final class net.corda.core.flows.NotarisationRequest extends java.lang.Object + public (List, net.corda.core.crypto.SecureHash) + @org.jetbrains.annotations.NotNull public final List getStatesToConsume() + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getTransactionId() + public final void verifySignature(net.corda.core.flows.NotarisationRequestSignature, net.corda.core.identity.Party) + public static final net.corda.core.flows.NotarisationRequest$Companion Companion +## +public static final class net.corda.core.flows.NotarisationRequest$Companion extends java.lang.Object +## +@net.corda.core.serialization.CordaSerializable public final class net.corda.core.flows.NotarisationRequestSignature extends java.lang.Object + public (net.corda.core.crypto.DigitalSignature$WithKey, int) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.DigitalSignature$WithKey component1() + public final int component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.NotarisationRequestSignature copy(net.corda.core.crypto.DigitalSignature$WithKey, int) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.DigitalSignature$WithKey getDigitalSignature() + public final int getPlatformVersion() + public int hashCode() + public String toString() +## @net.corda.core.flows.InitiatingFlow public final class net.corda.core.flows.NotaryChangeFlow extends net.corda.core.flows.AbstractStateReplacementFlow$Instigator public (net.corda.core.contracts.StateAndRef, net.corda.core.identity.Party, net.corda.core.utilities.ProgressTracker) @org.jetbrains.annotations.NotNull protected net.corda.core.flows.AbstractStateReplacementFlow$UpgradeTx assembleTx() @@ -1324,11 +1382,20 @@ public @interface net.corda.core.flows.InitiatingFlow @org.jetbrains.annotations.NotNull public String toString() ## @net.corda.core.serialization.CordaSerializable public static final class net.corda.core.flows.NotaryError$General extends net.corda.core.flows.NotaryError - public (String) - @org.jetbrains.annotations.NotNull public final String component1() - @org.jetbrains.annotations.NotNull public final net.corda.core.flows.NotaryError$General copy(String) + public (Throwable) + @org.jetbrains.annotations.NotNull public final Throwable component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.NotaryError$General copy(Throwable) public boolean equals(Object) - @org.jetbrains.annotations.NotNull public final String getCause() + @org.jetbrains.annotations.NotNull public final Throwable getCause() + public int hashCode() + @org.jetbrains.annotations.NotNull public String toString() +## +@net.corda.core.serialization.CordaSerializable public static final class net.corda.core.flows.NotaryError$RequestSignatureInvalid extends net.corda.core.flows.NotaryError + public (Throwable) + @org.jetbrains.annotations.NotNull public final Throwable component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.NotaryError$RequestSignatureInvalid copy(Throwable) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final Throwable getCause() public int hashCode() @org.jetbrains.annotations.NotNull public String toString() ## @@ -1370,7 +1437,10 @@ public final class net.corda.core.flows.NotaryFlow extends java.lang.Object public (net.corda.core.transactions.SignedTransaction) public (net.corda.core.transactions.SignedTransaction, net.corda.core.utilities.ProgressTracker) @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public List call() + @org.jetbrains.annotations.NotNull protected final net.corda.core.identity.Party checkTransaction() @org.jetbrains.annotations.NotNull public net.corda.core.utilities.ProgressTracker getProgressTracker() + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull protected final net.corda.core.utilities.UntrustworthyData notarise(net.corda.core.identity.Party) + @org.jetbrains.annotations.NotNull protected final List validateResponse(net.corda.core.utilities.UntrustworthyData, net.corda.core.identity.Party) public static final net.corda.core.flows.NotaryFlow$Client$Companion Companion ## public static final class net.corda.core.flows.NotaryFlow$Client$Companion extends java.lang.Object @@ -1747,14 +1817,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 (int, List, int, int, java.time.Instant, int) + public (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() @@ -1762,6 +1833,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() ## @@ -1803,6 +1875,7 @@ public @interface net.corda.core.messaging.RPCReturnsObservables @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.TransactionSignature createSignature(net.corda.core.transactions.FilteredTransaction, java.security.PublicKey) @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.TransactionSignature createSignature(net.corda.core.transactions.SignedTransaction) @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.TransactionSignature createSignature(net.corda.core.transactions.SignedTransaction, java.security.PublicKey) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.cordapp.CordappContext getAppContext() @org.jetbrains.annotations.NotNull public abstract java.time.Clock getClock() @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.ContractUpgradeService getContractUpgradeService() @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.KeyManagementService getKeyManagementService() @@ -1825,6 +1898,7 @@ public @interface net.corda.core.messaging.RPCReturnsObservables @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.AttachmentStorage getAttachments() @org.jetbrains.annotations.NotNull public abstract net.corda.core.cordapp.CordappProvider getCordappProvider() @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.IdentityService getIdentityService() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.NetworkParameters getNetworkParameters() ## @net.corda.core.DoNotImplement public interface net.corda.core.node.StateLoader @org.jetbrains.annotations.NotNull public abstract net.corda.core.contracts.TransactionState loadState(net.corda.core.contracts.StateRef) @@ -1837,9 +1911,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) ## @@ -2816,7 +2890,7 @@ public final class net.corda.core.serialization.ObjectWithCompatibleContext exte public final class net.corda.core.serialization.SerializationAPIKt extends java.lang.Object @org.jetbrains.annotations.NotNull public static final net.corda.core.serialization.SerializedBytes serialize(Object, net.corda.core.serialization.SerializationFactory, net.corda.core.serialization.SerializationContext) ## -public interface net.corda.core.serialization.SerializationContext +@net.corda.core.DoNotImplement public interface net.corda.core.serialization.SerializationContext @org.jetbrains.annotations.NotNull public abstract ClassLoader getDeserializationClassLoader() public abstract boolean getObjectReferencesEnabled() @org.jetbrains.annotations.NotNull public abstract net.corda.core.utilities.ByteSequence getPreferredSerializationVersion() @@ -2921,7 +2995,7 @@ public static final class net.corda.core.serialization.SingletonSerializationTok @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getId() @org.jetbrains.annotations.NotNull public final String getReason() ## -@net.corda.core.DoNotImplement public abstract class net.corda.core.transactions.CoreTransaction extends net.corda.core.transactions.BaseTransaction +@net.corda.core.serialization.CordaSerializable @net.corda.core.DoNotImplement public abstract class net.corda.core.transactions.CoreTransaction extends net.corda.core.transactions.BaseTransaction public () @org.jetbrains.annotations.NotNull public abstract List getInputs() ## @@ -2967,6 +3041,7 @@ public static final class net.corda.core.transactions.FilteredTransaction$Compan ## @net.corda.core.serialization.CordaSerializable @net.corda.core.DoNotImplement public final class net.corda.core.transactions.LedgerTransaction extends net.corda.core.transactions.FullTransaction public (List, List, List, List, net.corda.core.crypto.SecureHash, net.corda.core.identity.Party, net.corda.core.contracts.TimeWindow, net.corda.core.contracts.PrivacySalt) + public (List, List, List, List, net.corda.core.crypto.SecureHash, net.corda.core.identity.Party, net.corda.core.contracts.TimeWindow, net.corda.core.contracts.PrivacySalt, net.corda.core.node.NetworkParameters) @org.jetbrains.annotations.NotNull public final List commandsOfType(Class) @org.jetbrains.annotations.NotNull public final List component1() @org.jetbrains.annotations.NotNull public final List component2() @@ -2977,6 +3052,7 @@ public static final class net.corda.core.transactions.FilteredTransaction$Compan @org.jetbrains.annotations.Nullable public final net.corda.core.contracts.TimeWindow component7() @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.PrivacySalt component8() @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.LedgerTransaction copy(List, List, List, List, net.corda.core.crypto.SecureHash, net.corda.core.identity.Party, net.corda.core.contracts.TimeWindow, net.corda.core.contracts.PrivacySalt) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.LedgerTransaction copy(List, List, List, List, net.corda.core.crypto.SecureHash, net.corda.core.identity.Party, net.corda.core.contracts.TimeWindow, net.corda.core.contracts.PrivacySalt, net.corda.core.node.NetworkParameters) public boolean equals(Object) @org.jetbrains.annotations.NotNull public final List filterCommands(Class, function.Predicate) @org.jetbrains.annotations.NotNull public final List filterInRefs(Class, function.Predicate) @@ -3163,7 +3239,7 @@ public class net.corda.core.transactions.TransactionBuilder extends java.lang.Ob public abstract void verifyRequiredSignatures() public abstract void verifySignaturesExcept(Collection) ## -@net.corda.core.DoNotImplement public abstract class net.corda.core.transactions.TraversableTransaction extends net.corda.core.transactions.CoreTransaction +@net.corda.core.serialization.CordaSerializable @net.corda.core.DoNotImplement public abstract class net.corda.core.transactions.TraversableTransaction extends net.corda.core.transactions.CoreTransaction public (List) @org.jetbrains.annotations.NotNull public final List getAttachments() @org.jetbrains.annotations.NotNull public final List getAvailableComponentGroups() @@ -3185,7 +3261,7 @@ public class net.corda.core.transactions.TransactionBuilder extends java.lang.Ob @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.PrivacySalt getPrivacySalt() @org.jetbrains.annotations.NotNull public final Set getRequiredSigningKeys() public int hashCode() - @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.LedgerTransaction toLedgerTransaction(kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function1) + @kotlin.Deprecated @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.LedgerTransaction toLedgerTransaction(kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function1) @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.LedgerTransaction toLedgerTransaction(net.corda.core.node.ServicesForResolution) @org.jetbrains.annotations.NotNull public String toString() public static final net.corda.core.transactions.WireTransaction$Companion Companion @@ -3655,12 +3731,11 @@ public final class net.corda.testing.driver.Driver extends java.lang.Object ## public final class net.corda.testing.driver.DriverParameters extends java.lang.Object public () - public (boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, Map, boolean, boolean, boolean, boolean, List, List, net.corda.testing.driver.JmxPolicy, int) + public (boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, Map, boolean, boolean, boolean, List, List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters) public final boolean component1() @org.jetbrains.annotations.NotNull public final List component10() - @org.jetbrains.annotations.NotNull public final List component11() - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.JmxPolicy component12() - public final int component13() + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.JmxPolicy component11() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.NetworkParameters component12() @org.jetbrains.annotations.NotNull public final java.nio.file.Path component2() @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.PortAllocation component3() @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.PortAllocation component4() @@ -3668,15 +3743,14 @@ public final class net.corda.testing.driver.DriverParameters extends java.lang.O public final boolean component6() public final boolean component7() public final boolean component8() - public final boolean component9() - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters copy(boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, Map, boolean, boolean, boolean, boolean, List, List, net.corda.testing.driver.JmxPolicy, int) + @org.jetbrains.annotations.NotNull public final List component9() + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters copy(boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, Map, boolean, boolean, boolean, List, List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters) public boolean equals(Object) @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.PortAllocation getDebugPortAllocation() @org.jetbrains.annotations.NotNull public final java.nio.file.Path getDriverDirectory() @org.jetbrains.annotations.NotNull public final List getExtraCordappPackagesToScan() - public final boolean getInitialiseSerialization() @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.JmxPolicy getJmxPolicy() - public final int getMaxTransactionSize() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.NetworkParameters getNetworkParameters() @org.jetbrains.annotations.NotNull public final List getNotarySpecs() @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.PortAllocation getPortAllocation() public final boolean getStartNodesInProcess() @@ -3685,22 +3759,21 @@ public final class net.corda.testing.driver.DriverParameters extends java.lang.O public final boolean getWaitForAllNodesToFinish() public int hashCode() public final boolean isDebug() - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters setDebugPortAllocation(net.corda.testing.driver.PortAllocation) - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters setDriverDirectory(java.nio.file.Path) - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters setExtraCordappPackagesToScan(List) - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters setInitialiseSerialization(boolean) - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters setIsDebug(boolean) - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters setJmxPolicy(net.corda.testing.driver.JmxPolicy) - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters setNotarySpecs(List) - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters setPortAllocation(net.corda.testing.driver.PortAllocation) - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters setStartNodesInProcess(boolean) - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters setSystemProperties(Map) - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters setUseTestClock(boolean) - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters setWaitForAllNodesToFinish(boolean) public String toString() + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters withDebugPortAllocation(net.corda.testing.driver.PortAllocation) + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters withDriverDirectory(java.nio.file.Path) + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters withExtraCordappPackagesToScan(List) + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters withIsDebug(boolean) + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters withJmxPolicy(net.corda.testing.driver.JmxPolicy) + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters withNetworkParameters(net.corda.core.node.NetworkParameters) + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters withNotarySpecs(List) + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters withPortAllocation(net.corda.testing.driver.PortAllocation) + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters withStartNodesInProcess(boolean) + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters withSystemProperties(Map) + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters withUseTestClock(boolean) + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters withWaitForAllNodesToFinish(boolean) ## @net.corda.core.DoNotImplement public interface net.corda.testing.driver.InProcess extends net.corda.testing.driver.NodeHandle - @org.jetbrains.annotations.NotNull public abstract net.corda.nodeapi.internal.persistence.CordaPersistence getDatabase() @org.jetbrains.annotations.NotNull public abstract net.corda.node.services.api.StartedNodeServices getServices() @org.jetbrains.annotations.NotNull public abstract rx.Observable registerInitiatedFlow(Class) ## @@ -3743,13 +3816,13 @@ public final class net.corda.testing.driver.NodeParameters extends java.lang.Obj @org.jetbrains.annotations.Nullable public final Boolean getStartInSameProcess() @org.jetbrains.annotations.NotNull public final net.corda.node.services.config.VerifierType getVerifierType() public int hashCode() - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.NodeParameters setCustomerOverrides(Map) - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.NodeParameters setMaximumHeapSize(String) - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.NodeParameters setProvidedName(net.corda.core.identity.CordaX500Name) - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.NodeParameters setRpcUsers(List) - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.NodeParameters setStartInSameProcess(Boolean) - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.NodeParameters setVerifierType(net.corda.node.services.config.VerifierType) public String toString() + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.NodeParameters withCustomerOverrides(Map) + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.NodeParameters withMaximumHeapSize(String) + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.NodeParameters withProvidedName(net.corda.core.identity.CordaX500Name) + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.NodeParameters withRpcUsers(List) + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.NodeParameters withStartInSameProcess(Boolean) + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.NodeParameters withVerifierType(net.corda.node.services.config.VerifierType) ## public final class net.corda.testing.driver.NotaryHandle extends java.lang.Object public (net.corda.core.identity.Party, boolean, net.corda.core.concurrent.CordaFuture) @@ -3805,7 +3878,6 @@ public static final class net.corda.testing.node.ClusterSpec$Raft extends net.co public String toString() ## @javax.annotation.concurrent.ThreadSafe public final class net.corda.testing.node.InMemoryMessagingNetwork extends net.corda.core.serialization.SingletonSerializeAsToken - public (boolean, net.corda.testing.node.InMemoryMessagingNetwork$ServicePeerAllocationStrategy, org.apache.activemq.artemis.utils.ReusableLatch) @org.jetbrains.annotations.NotNull public synchronized final List getEndpoints() @org.jetbrains.annotations.NotNull public final rx.Observable getReceivedMessages() @org.jetbrains.annotations.NotNull public final rx.Observable getSentMessages() @@ -3888,10 +3960,6 @@ public static final class net.corda.testing.node.InMemoryMessagingNetwork$Servic @org.jetbrains.annotations.Nullable public abstract net.corda.testing.node.InMemoryMessagingNetwork$MessageTransfer pumpReceive(boolean) public abstract void stop() ## -public static final class net.corda.testing.node.InMemoryMessagingNetwork$pumpSend$$inlined$schedule$1 extends java.util.TimerTask - public (net.corda.testing.node.InMemoryMessagingNetwork, net.corda.testing.node.InMemoryMessagingNetwork$MessageTransfer, net.corda.core.internal.concurrent.OpenFuture) - public void run() -## public class net.corda.testing.node.MessagingServiceSpy extends java.lang.Object implements net.corda.node.services.messaging.MessagingService public (net.corda.node.services.messaging.MessagingService) @org.jetbrains.annotations.NotNull public net.corda.node.services.messaging.MessageHandlerRegistration addMessageHandler(String, kotlin.jvm.functions.Function2) @@ -3916,7 +3984,7 @@ public final class net.corda.testing.node.MockKeyManagementService extends net.c public class net.corda.testing.node.MockNetwork extends java.lang.Object public (List) public (List, net.corda.testing.node.MockNetworkParameters) - public (List, net.corda.testing.node.MockNetworkParameters, boolean, boolean, net.corda.testing.node.InMemoryMessagingNetwork$ServicePeerAllocationStrategy, boolean, List, int) + public (List, net.corda.testing.node.MockNetworkParameters, boolean, boolean, net.corda.testing.node.InMemoryMessagingNetwork$ServicePeerAllocationStrategy, List, net.corda.core.node.NetworkParameters) @org.jetbrains.annotations.NotNull public final java.nio.file.Path baseDirectory(int) @org.jetbrains.annotations.NotNull public final net.corda.testing.node.StartedMockNode createNode() @org.jetbrains.annotations.NotNull public final net.corda.testing.node.StartedMockNode createNode(net.corda.core.identity.CordaX500Name) @@ -3937,8 +4005,7 @@ public class net.corda.testing.node.MockNetwork extends java.lang.Object @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party getDefaultNotaryIdentity() @org.jetbrains.annotations.NotNull public final net.corda.testing.node.StartedMockNode getDefaultNotaryNode() @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters getDefaultParameters() - public final boolean getInitialiseSerialization() - public final int getMaxTransactionSize() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.NetworkParameters getNetworkParameters() public final boolean getNetworkSendManuallyPumped() public final int getNextNodeId() @org.jetbrains.annotations.NotNull public final List getNotaryNodes() @@ -3965,29 +4032,26 @@ public final class net.corda.testing.node.MockNetworkNotarySpec extends java.lan ## public final class net.corda.testing.node.MockNetworkParameters extends java.lang.Object public () - public (boolean, boolean, net.corda.testing.node.InMemoryMessagingNetwork$ServicePeerAllocationStrategy, boolean, List, int) + public (boolean, boolean, net.corda.testing.node.InMemoryMessagingNetwork$ServicePeerAllocationStrategy, List, net.corda.core.node.NetworkParameters) public final boolean component1() public final boolean component2() @org.jetbrains.annotations.NotNull public final net.corda.testing.node.InMemoryMessagingNetwork$ServicePeerAllocationStrategy component3() - public final boolean component4() - @org.jetbrains.annotations.NotNull public final List component5() - public final int component6() - @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters copy(boolean, boolean, net.corda.testing.node.InMemoryMessagingNetwork$ServicePeerAllocationStrategy, boolean, List, int) + @org.jetbrains.annotations.NotNull public final List component4() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.NetworkParameters component5() + @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters copy(boolean, boolean, net.corda.testing.node.InMemoryMessagingNetwork$ServicePeerAllocationStrategy, List, net.corda.core.node.NetworkParameters) public boolean equals(Object) - public final boolean getInitialiseSerialization() - public final int getMaxTransactionSize() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.NetworkParameters getNetworkParameters() public final boolean getNetworkSendManuallyPumped() @org.jetbrains.annotations.NotNull public final List getNotarySpecs() @org.jetbrains.annotations.NotNull public final net.corda.testing.node.InMemoryMessagingNetwork$ServicePeerAllocationStrategy getServicePeerAllocationStrategy() public final boolean getThreadPerNode() public int hashCode() - @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters setInitialiseSerialization(boolean) - @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters setMaxTransactionSize(int) - @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters setNetworkSendManuallyPumped(boolean) - @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters setNotarySpecs(List) - @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters setServicePeerAllocationStrategy(net.corda.testing.node.InMemoryMessagingNetwork$ServicePeerAllocationStrategy) - @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters setThreadPerNode(boolean) public String toString() + @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters withNetworkParameters(net.corda.core.node.NetworkParameters) + @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters withNetworkSendManuallyPumped(boolean) + @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters withNotarySpecs(List) + @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters withServicePeerAllocationStrategy(net.corda.testing.node.InMemoryMessagingNetwork$ServicePeerAllocationStrategy) + @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters withThreadPerNode(boolean) ## public final class net.corda.testing.node.MockNodeParameters extends java.lang.Object public () @@ -4005,11 +4069,11 @@ public final class net.corda.testing.node.MockNodeParameters extends java.lang.O @org.jetbrains.annotations.Nullable public final net.corda.core.identity.CordaX500Name getLegalName() @org.jetbrains.annotations.NotNull public final net.corda.node.VersionInfo getVersion() public int hashCode() - @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNodeParameters setConfigOverrides(kotlin.jvm.functions.Function1) - @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNodeParameters setEntropyRoot(java.math.BigInteger) - @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNodeParameters setForcedID(Integer) - @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNodeParameters setLegalName(net.corda.core.identity.CordaX500Name) public String toString() + @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNodeParameters withConfigOverrides(kotlin.jvm.functions.Function1) + @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNodeParameters withEntropyRoot(java.math.BigInteger) + @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNodeParameters withForcedID(Integer) + @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNodeParameters withLegalName(net.corda.core.identity.CordaX500Name) ## public class net.corda.testing.node.MockServices extends java.lang.Object implements net.corda.core.node.StateLoader, net.corda.core.node.ServiceHub public () @@ -4018,6 +4082,7 @@ public class net.corda.testing.node.MockServices extends java.lang.Object implem public (List, net.corda.core.identity.CordaX500Name, net.corda.core.node.services.IdentityService) public (net.corda.core.identity.CordaX500Name) public (net.corda.core.identity.CordaX500Name, net.corda.core.node.services.IdentityService) + public final void addMockCordapp(String) @org.jetbrains.annotations.NotNull public net.corda.core.transactions.SignedTransaction addSignature(net.corda.core.transactions.SignedTransaction) @org.jetbrains.annotations.NotNull public net.corda.core.transactions.SignedTransaction addSignature(net.corda.core.transactions.SignedTransaction, java.security.PublicKey) @org.jetbrains.annotations.NotNull public net.corda.core.serialization.SerializeAsToken cordaService(Class) @@ -4025,6 +4090,7 @@ public class net.corda.testing.node.MockServices extends java.lang.Object implem @org.jetbrains.annotations.NotNull public net.corda.core.crypto.TransactionSignature createSignature(net.corda.core.transactions.FilteredTransaction, java.security.PublicKey) @org.jetbrains.annotations.NotNull public net.corda.core.crypto.TransactionSignature createSignature(net.corda.core.transactions.SignedTransaction) @org.jetbrains.annotations.NotNull public net.corda.core.crypto.TransactionSignature createSignature(net.corda.core.transactions.SignedTransaction, java.security.PublicKey) + @org.jetbrains.annotations.NotNull public net.corda.core.cordapp.CordappContext getAppContext() @org.jetbrains.annotations.NotNull public final net.corda.testing.services.MockAttachmentStorage getAttachments() @org.jetbrains.annotations.NotNull public java.time.Clock getClock() @org.jetbrains.annotations.NotNull public net.corda.core.node.services.ContractUpgradeService getContractUpgradeService() @@ -4033,9 +4099,9 @@ public class net.corda.testing.node.MockServices extends java.lang.Object implem @org.jetbrains.annotations.NotNull public net.corda.core.node.services.IdentityService getIdentityService() @org.jetbrains.annotations.NotNull public net.corda.core.node.services.KeyManagementService getKeyManagementService() @org.jetbrains.annotations.NotNull public static final net.corda.node.VersionInfo getMOCK_VERSION_INFO() - @org.jetbrains.annotations.NotNull public final net.corda.testing.services.MockCordappProvider getMockCordappProvider() @org.jetbrains.annotations.NotNull public net.corda.core.node.NodeInfo getMyInfo() @org.jetbrains.annotations.NotNull public net.corda.core.node.services.NetworkMapCache getNetworkMapCache() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.NetworkParameters getNetworkParameters() @org.jetbrains.annotations.NotNull public net.corda.core.node.services.TransactionVerifierService getTransactionVerifierService() @org.jetbrains.annotations.NotNull public net.corda.node.services.api.WritableTransactionStorage getValidatedTransactions() @org.jetbrains.annotations.NotNull public net.corda.core.node.services.VaultService getVaultService() @@ -4074,6 +4140,7 @@ public static final class net.corda.testing.node.MockServicesKt$createMockCordaS @org.jetbrains.annotations.NotNull public net.corda.core.crypto.TransactionSignature createSignature(net.corda.core.transactions.FilteredTransaction, java.security.PublicKey) @org.jetbrains.annotations.NotNull public net.corda.core.crypto.TransactionSignature createSignature(net.corda.core.transactions.SignedTransaction) @org.jetbrains.annotations.NotNull public net.corda.core.crypto.TransactionSignature createSignature(net.corda.core.transactions.SignedTransaction, java.security.PublicKey) + @org.jetbrains.annotations.NotNull public net.corda.core.cordapp.CordappContext getAppContext() @org.jetbrains.annotations.NotNull public net.corda.core.node.services.AttachmentStorage getAttachments() @org.jetbrains.annotations.NotNull public java.time.Clock getClock() @org.jetbrains.annotations.NotNull public net.corda.core.node.services.ContractUpgradeService getContractUpgradeService() @@ -4082,6 +4149,7 @@ public static final class net.corda.testing.node.MockServicesKt$createMockCordaS @org.jetbrains.annotations.NotNull public net.corda.core.node.services.KeyManagementService getKeyManagementService() @org.jetbrains.annotations.NotNull public net.corda.core.node.NodeInfo getMyInfo() @org.jetbrains.annotations.NotNull public net.corda.core.node.services.NetworkMapCache getNetworkMapCache() + @org.jetbrains.annotations.NotNull public net.corda.core.node.NetworkParameters getNetworkParameters() @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockServices getServiceHub() @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializeAsToken getServiceInstance() @org.jetbrains.annotations.NotNull public net.corda.core.node.services.TransactionVerifierService getTransactionVerifierService() @@ -4139,7 +4207,6 @@ public final class net.corda.testing.node.NotarySpec extends java.lang.Object ## public final class net.corda.testing.node.StartedMockNode extends java.lang.Object @org.jetbrains.annotations.NotNull public final List findStateMachines(Class) - @org.jetbrains.annotations.NotNull public final net.corda.nodeapi.internal.persistence.CordaPersistence getDatabase() public final int getId() @org.jetbrains.annotations.NotNull public final net.corda.core.node.NodeInfo getInfo() @org.jetbrains.annotations.NotNull public final net.corda.node.services.messaging.MessagingService getNetwork() @@ -4148,11 +4215,12 @@ public final class net.corda.testing.node.StartedMockNode extends java.lang.Obje @org.jetbrains.annotations.NotNull public final rx.Observable registerInitiatedFlow(Class) public final void setMessagingServiceSpy(net.corda.testing.node.MessagingServiceSpy) public final void stop() + public final Object transaction(kotlin.jvm.functions.Function0) public static final net.corda.testing.node.StartedMockNode$Companion Companion ## public static final class net.corda.testing.node.StartedMockNode$Companion extends java.lang.Object ## -@javax.annotation.concurrent.ThreadSafe public final class net.corda.testing.node.TestClock extends net.corda.node.internal.MutableClock +@javax.annotation.concurrent.ThreadSafe public final class net.corda.testing.node.TestClock extends net.corda.node.MutableClock public (java.time.Clock) public synchronized final void advanceBy(java.time.Duration) public synchronized final void setTo(java.time.Instant) @@ -4411,24 +4479,7 @@ public static final class net.corda.testing.core.SerializationEnvironmentRule$Co public static final class net.corda.testing.core.SerializationEnvironmentRule$apply$1 extends org.junit.runners.model.Statement public void evaluate() ## -public final class net.corda.testing.core.SerializationTestHelpersKt extends java.lang.Object - @org.jetbrains.annotations.NotNull public static final net.corda.testing.core.GlobalSerializationEnvironment setGlobalSerialization(boolean) -## -public static final class net.corda.testing.core.SerializationTestHelpersKt$createTestSerializationEnv$1 extends net.corda.core.serialization.internal.SerializationEnvironmentImpl - @org.jetbrains.annotations.NotNull public String toString() -## -public static final class net.corda.testing.core.SerializationTestHelpersKt$setGlobalSerialization$1 extends java.lang.Object implements net.corda.testing.core.GlobalSerializationEnvironment, net.corda.core.serialization.internal.SerializationEnvironment - @org.jetbrains.annotations.NotNull public net.corda.core.serialization.SerializationContext getCheckpointContext() - @org.jetbrains.annotations.NotNull public net.corda.core.serialization.SerializationContext getP2pContext() - @org.jetbrains.annotations.NotNull public net.corda.core.serialization.SerializationContext getRpcClientContext() - @org.jetbrains.annotations.NotNull public net.corda.core.serialization.SerializationContext getRpcServerContext() - @org.jetbrains.annotations.NotNull public net.corda.core.serialization.SerializationFactory getSerializationFactory() - @org.jetbrains.annotations.NotNull public net.corda.core.serialization.SerializationContext getStorageContext() - public void unset() -## public final class net.corda.testing.core.TestConstants extends java.lang.Object - @org.jetbrains.annotations.NotNull public static final net.corda.nodeapi.internal.crypto.CertificateAndKeyPair getDEV_INTERMEDIATE_CA() - @org.jetbrains.annotations.NotNull public static final net.corda.nodeapi.internal.crypto.CertificateAndKeyPair getDEV_ROOT_CA() @kotlin.jvm.JvmField @org.jetbrains.annotations.NotNull public static final net.corda.core.identity.CordaX500Name ALICE_NAME @kotlin.jvm.JvmField @org.jetbrains.annotations.NotNull public static final net.corda.core.identity.CordaX500Name BOB_NAME @kotlin.jvm.JvmField @org.jetbrains.annotations.NotNull public static final net.corda.core.identity.CordaX500Name BOC_NAME @@ -4583,6 +4634,7 @@ public static final class net.corda.testing.dsl.TestTransactionDSLInterpreter$se @org.jetbrains.annotations.NotNull public net.corda.core.node.services.AttachmentStorage getAttachments() @org.jetbrains.annotations.NotNull public net.corda.core.cordapp.CordappProvider getCordappProvider() @org.jetbrains.annotations.NotNull public net.corda.core.node.services.IdentityService getIdentityService() + @org.jetbrains.annotations.NotNull public net.corda.core.node.NetworkParameters getNetworkParameters() @org.jetbrains.annotations.NotNull public net.corda.core.contracts.TransactionState loadState(net.corda.core.contracts.StateRef) @org.jetbrains.annotations.NotNull public Set loadStates(Set) ## @@ -4670,6 +4722,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) @@ -4678,12 +4731,3 @@ public final class net.corda.testing.services.MockAttachmentStorage extends net. public static final class net.corda.testing.services.MockAttachmentStorage$Companion extends java.lang.Object public final byte[] getBytes(java.io.InputStream) ## -public static final class net.corda.testing.services.MockAttachmentStorage$openAttachment$1 extends net.corda.core.internal.AbstractAttachment - @org.jetbrains.annotations.NotNull public net.corda.core.crypto.SecureHash getId() -## -public final class net.corda.testing.services.MockCordappProvider extends net.corda.node.internal.cordapp.CordappProviderImpl - public (net.corda.node.internal.cordapp.CordappLoader, net.corda.core.node.services.AttachmentStorage) - public final void addMockCordapp(String, net.corda.testing.services.MockAttachmentStorage) - @org.jetbrains.annotations.Nullable public net.corda.core.crypto.SecureHash getContractAttachmentID(String) - @org.jetbrains.annotations.NotNull public final List getCordappRegistry() -## diff --git a/.ci/check-api-changes.sh b/.ci/check-api-changes.sh index 987d0ced3c..5896f6ebbf 100755 --- a/.ci/check-api-changes.sh +++ b/.ci/check-api-changes.sh @@ -31,8 +31,7 @@ if [ $removalCount -gt 0 ]; then fi # Adding new abstract methods could also break the API. -# However, first exclude anything with the @DoNotImplement annotation. - +# However, first exclude classes marked with the @DoNotImplement annotation function forUserImpl() { awk '/DoNotImplement/,/^##/{ next }{ print }' $1 } @@ -45,13 +44,28 @@ $newAbstracts EOF ` +#Get a list of any methods that expose classes in .internal. namespaces, and any classes which extend/implement +#an internal class +newInternalExposures=$(echo "$userDiffContents" | grep "^+" | grep "\.internal\." ) + +internalCount=`grep -v "^$" < + + @@ -37,6 +39,7 @@ + @@ -153,8 +156,8 @@ - - + + @@ -163,4 +166,4 @@ - + \ No newline at end of file diff --git a/build.gradle b/build.gradle index b20d78d4d3..0a9307d4c6 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,9 @@ buildscript { // // TODO: Sort this alphabetically. ext.kotlin_version = constants.getProperty("kotlinVersion") - ext.quasar_version = '0.7.9' + // use our fork of quasar + ext.quasar_group = 'com.github.corda.quasar' + ext.quasar_version = '7629695563deae6cc95adcfbebcbc8322fd0241a' // gradle-capsule-plugin:1.0.2 contains capsule:1.0.1 // TODO: Upgrade gradle-capsule-plugin to a version with capsule:1.0.3 @@ -38,7 +40,6 @@ buildscript { ext.jackson_version = '2.9.3' ext.jetty_version = '9.4.7.v20170914' ext.jersey_version = '2.25' - ext.jolokia_version = '1.3.7' ext.assertj_version = '3.8.0' ext.slf4j_version = '1.7.25' ext.log4j_version = '2.9.1' @@ -69,6 +70,7 @@ buildscript { ext.docker_compose_rule_version = '0.33.0' ext.selenium_version = '3.8.1' ext.ghostdriver_version = '2.1.0' + ext.eaagentloader_version = '1.0.3' // Update 121 is required for ObjectInputFilter and at time of writing 131 was latest: ext.java8_minUpdateVersion = '131' @@ -114,7 +116,6 @@ ext { apply plugin: 'project-report' apply plugin: 'com.github.ben-manes.versions' apply plugin: 'net.corda.plugins.publish-utils' -apply plugin: 'net.corda.plugins.cordformation' apply plugin: 'maven-publish' apply plugin: 'com.jfrog.artifactory' @@ -172,6 +173,10 @@ allprojects { if (System.getProperty("test.maxParallelForks") != null) { maxParallelForks = Integer.valueOf(System.getProperty("test.maxParallelForks")) } + + if (project.path.startsWith(':experimental') && System.getProperty("experimental.test.enable") == null) { + enabled = false + } } group 'net.corda' @@ -260,30 +265,6 @@ tasks.withType(Test) { reports.html.destination = file("${reporting.baseDir}/${name}") } -task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { - directory "./build/nodes" - node { - name "O=Controller,OU=corda,L=London,C=GB" - notary = [validating : true] - p2pPort 10002 - cordapps = [] - } - node { - name "O=Bank A,OU=corda,L=London,C=GB" - p2pPort 10012 - rpcPort 10013 - webPort 10014 - cordapps = [] - } - node { - name "O=Bank B,OU=corda,L=London,C=GB" - p2pAddress "localhost:10007" - rpcAddress "localhost:10008" - webAddress "localhost:10009" - cordapps = [] - } -} - bintrayConfig { user = System.getenv('CORDA_BINTRAY_USER') key = System.getenv('CORDA_BINTRAY_KEY') diff --git a/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt b/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt index 41a41ed327..14889e536d 100644 --- a/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt +++ b/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt @@ -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) diff --git a/client/rpc/build.gradle b/client/rpc/build.gradle index bd234e8ee1..3b585cf164 100644 --- a/client/rpc/build.gradle +++ b/client/rpc/build.gradle @@ -15,6 +15,14 @@ configurations { smokeTestRuntime.extendsFrom runtime } +compileKotlin { + kotlinOptions.jvmTarget = "1.8" +} + +compileTestKotlin { + kotlinOptions.jvmTarget = "1.8" +} + sourceSets { integrationTest { kotlin { diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt index 45b5bd3d0b..0ac85510f9 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt @@ -13,7 +13,7 @@ import net.corda.core.utilities.* import net.corda.node.services.messaging.RPCServerConfiguration import net.corda.nodeapi.RPCApi import net.corda.testing.core.SerializationEnvironmentRule -import net.corda.testing.internal.* +import net.corda.testing.internal.testThreadFactory import net.corda.testing.node.internal.* import org.apache.activemq.artemis.api.core.SimpleString import org.junit.After @@ -30,6 +30,7 @@ import java.util.concurrent.Executors import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicLong class RPCStabilityTests { @Rule @@ -127,7 +128,7 @@ class RPCStabilityTests { rpcDriver { fun startAndCloseServer(broker: RpcBrokerHandle) { startRpcServerWithBrokerRunning( - configuration = RPCServerConfiguration.default.copy(consumerPoolSize = 1, producerPoolBound = 1), + configuration = RPCServerConfiguration.default, ops = DummyOps, brokerHandle = broker ).rpcServer.close() @@ -148,7 +149,7 @@ class RPCStabilityTests { @Test fun `rpc client close doesnt leak broker resources`() { rpcDriver { - val server = startRpcServer(configuration = RPCServerConfiguration.default.copy(consumerPoolSize = 1, producerPoolBound = 1), ops = DummyOps).get() + val server = startRpcServer(configuration = RPCServerConfiguration.default, ops = DummyOps).get() RPCClient(server.broker.hostAndPort!!).start(RPCOps::class.java, rpcTestUser.username, rpcTestUser.password).close() val initial = server.broker.getStats() repeat(100) { @@ -337,11 +338,12 @@ class RPCStabilityTests { val request = RPCApi.ClientToServer.RpcRequest( clientAddress = SimpleString(myQueue), methodName = SlowConsumerRPCOps::streamAtInterval.name, - serialisedArguments = listOf(10.millis, 123456).serialize(context = SerializationDefaults.RPC_SERVER_CONTEXT).bytes, + serialisedArguments = listOf(10.millis, 123456).serialize(context = SerializationDefaults.RPC_SERVER_CONTEXT), replyId = Trace.InvocationId.newInstance(), sessionId = Trace.SessionId.newInstance() ) request.writeToClientMessage(message) + message.putLongProperty(RPCApi.DEDUPLICATION_SEQUENCE_NUMBER_FIELD_NAME, 0) producer.send(message) session.commit() @@ -350,6 +352,79 @@ class RPCStabilityTests { } } + @Test + fun `deduplication in the server`() { + rpcDriver { + val server = startRpcServer(ops = SlowConsumerRPCOpsImpl()).getOrThrow() + + // Construct an RPC client session manually + val myQueue = "${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.test.${random63BitValue()}" + val session = startArtemisSession(server.broker.hostAndPort!!) + session.createTemporaryQueue(myQueue, myQueue) + val consumer = session.createConsumer(myQueue, null, -1, -1, false) + val replies = ArrayList() + consumer.setMessageHandler { + replies.add(it) + it.acknowledge() + } + + val producer = session.createProducer(RPCApi.RPC_SERVER_QUEUE_NAME) + session.start() + + pollUntilClientNumber(server, 1) + + val message = session.createMessage(false) + val request = RPCApi.ClientToServer.RpcRequest( + clientAddress = SimpleString(myQueue), + methodName = DummyOps::protocolVersion.name, + serialisedArguments = emptyList().serialize(context = SerializationDefaults.RPC_SERVER_CONTEXT), + replyId = Trace.InvocationId.newInstance(), + sessionId = Trace.SessionId.newInstance() + ) + request.writeToClientMessage(message) + message.putLongProperty(RPCApi.DEDUPLICATION_SEQUENCE_NUMBER_FIELD_NAME, 0) + producer.send(message) + // duplicate the message + producer.send(message) + + pollUntilTrue("Number of replies is 1") { + replies.size == 1 + }.getOrThrow() + } + } + + @Test + fun `deduplication in the client`() { + rpcDriver { + val broker = startRpcBroker().getOrThrow() + + // Construct an RPC server session manually + val session = startArtemisSession(broker.hostAndPort!!) + val consumer = session.createConsumer(RPCApi.RPC_SERVER_QUEUE_NAME) + val producer = session.createProducer() + val dedupeId = AtomicLong(0) + consumer.setMessageHandler { + it.acknowledge() + val request = RPCApi.ClientToServer.fromClientMessage(it) + when (request) { + is RPCApi.ClientToServer.RpcRequest -> { + val reply = RPCApi.ServerToClient.RpcReply(request.replyId, Try.Success(0), "server") + val message = session.createMessage(false) + reply.writeToClientMessage(SerializationDefaults.RPC_SERVER_CONTEXT, message) + message.putLongProperty(RPCApi.DEDUPLICATION_SEQUENCE_NUMBER_FIELD_NAME, dedupeId.getAndIncrement()) + producer.send(request.clientAddress, message) + // duplicate the reply + producer.send(request.clientAddress, message) + } + is RPCApi.ClientToServer.ObservablesClosed -> { + } + } + } + session.start() + + startRpcClient(broker.hostAndPort!!).getOrThrow() + } + } } fun RPCDriverDSL.pollUntilClientNumber(server: RpcServerHandle, expected: Int) { diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt index ea4f446fa0..79ad14f5dd 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt @@ -44,8 +44,6 @@ data class RPCClientConfiguration( val reapInterval: Duration, /** The number of threads to use for observations (for executing [Observable.onNext]) */ val observationExecutorPoolSize: Int, - /** The maximum number of producers to create to handle outgoing messages */ - val producerPoolBound: Int, /** * Determines the concurrency level of the Observable Cache. This is exposed because it implicitly determines * the limit on the number of leaked observables reaped because of garbage collection per reaping. @@ -58,9 +56,12 @@ data class RPCClientConfiguration( val connectionRetryIntervalMultiplier: Double, /** Maximum retry interval */ val connectionMaxRetryInterval: Duration, + /** Maximum reconnect attempts on failover */ val maxReconnectAttempts: Int, /** Maximum file size */ - val maxFileSize: Int + val maxFileSize: Int, + /** The cache expiry of a deduplication watermark per client. */ + val deduplicationCacheExpiry: Duration ) { companion object { val unlimitedReconnectAttempts = -1 @@ -70,14 +71,14 @@ data class RPCClientConfiguration( trackRpcCallSites = false, reapInterval = 1.seconds, observationExecutorPoolSize = 4, - producerPoolBound = 1, cacheConcurrencyLevel = 8, connectionRetryInterval = 5.seconds, connectionRetryIntervalMultiplier = 1.5, connectionMaxRetryInterval = 3.minutes, maxReconnectAttempts = unlimitedReconnectAttempts, /** 10 MiB maximum allowed file size for attachments, including message headers. TODO: acquire this value from Network Map when supported. */ - maxFileSize = 10485760 + maxFileSize = 10485760, + deduplicationCacheExpiry = 1.days ) } } diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt index 813569dc89..2f65f21e40 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt @@ -15,7 +15,6 @@ import net.corda.client.rpc.RPCSinceVersion import net.corda.core.context.Actor import net.corda.core.context.Trace import net.corda.core.context.Trace.InvocationId -import net.corda.core.internal.LazyPool import net.corda.core.internal.LazyStickyPool import net.corda.core.internal.LifeCycle import net.corda.core.internal.ThreadBox @@ -26,14 +25,12 @@ import net.corda.core.utilities.Try import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug import net.corda.core.utilities.getOrThrow -import net.corda.nodeapi.ArtemisConsumer -import net.corda.nodeapi.ArtemisProducer import net.corda.nodeapi.RPCApi +import net.corda.nodeapi.internal.DeduplicationChecker import org.apache.activemq.artemis.api.core.RoutingType import org.apache.activemq.artemis.api.core.SimpleString +import org.apache.activemq.artemis.api.core.client.* import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE -import org.apache.activemq.artemis.api.core.client.ClientMessage -import org.apache.activemq.artemis.api.core.client.ServerLocator import rx.Notification import rx.Observable import rx.subjects.UnicastSubject @@ -43,6 +40,7 @@ import java.time.Instant import java.util.* import java.util.concurrent.* import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicLong import kotlin.reflect.jvm.javaMethod /** @@ -111,6 +109,8 @@ class RPCClientProxyHandler( // Used for reaping private var reaperExecutor: ScheduledExecutorService? = null + // Used for sending + private var sendExecutor: ExecutorService? = null // A sticky pool for running Observable.onNext()s. We need the stickiness to preserve the observation ordering. private val observationExecutorThreadFactory = ThreadFactoryBuilder().setNameFormat("rpc-client-observation-pool-%d").setDaemon(true).build() @@ -161,22 +161,14 @@ class RPCClientProxyHandler( build() } - // We cannot pool consumers as we need to preserve the original muxed message order. - // TODO We may need to pool these somehow anyway, otherwise if the server sends many big messages in parallel a - // single consumer may be starved for flow control credits. Recheck this once Artemis's large message streaming is - // integrated properly. - private var sessionAndConsumer: ArtemisConsumer? = null - // Pool producers to reduce contention on the client side. - private val sessionAndProducerPool = LazyPool(bound = rpcConfiguration.producerPoolBound) { - // Note how we create new sessions *and* session factories per producer. - // We cannot simply pool producers on one session because sessions are single threaded. - // We cannot simply pool sessions on one session factory because flow control credits are tied to factories, so - // sessions tend to starve each other when used concurrently. - val sessionFactory = serverLocator.createSessionFactory() - val session = sessionFactory.createSession(rpcUsername, rpcPassword, false, true, true, false, DEFAULT_ACK_BATCH_SIZE) - session.start() - ArtemisProducer(sessionFactory, session, session.createProducer(RPCApi.RPC_SERVER_QUEUE_NAME)) - } + private var sessionFactory: ClientSessionFactory? = null + private var producerSession: ClientSession? = null + private var consumerSession: ClientSession? = null + private var rpcProducer: ClientProducer? = null + private var rpcConsumer: ClientConsumer? = null + + private val deduplicationChecker = DeduplicationChecker(rpcConfiguration.deduplicationCacheExpiry) + private val deduplicationSequenceNumber = AtomicLong(0) /** * Start the client. This creates the per-client queue, starts the consumer session and the reaper. @@ -187,22 +179,25 @@ class RPCClientProxyHandler( 1, ThreadFactoryBuilder().setNameFormat("rpc-client-reaper-%d").setDaemon(true).build() ) + sendExecutor = Executors.newSingleThreadExecutor( + ThreadFactoryBuilder().setNameFormat("rpc-client-sender-%d").build() + ) reaperScheduledFuture = reaperExecutor!!.scheduleAtFixedRate( this::reapObservablesAndNotify, rpcConfiguration.reapInterval.toMillis(), rpcConfiguration.reapInterval.toMillis(), TimeUnit.MILLISECONDS ) - sessionAndProducerPool.run { - it.session.createTemporaryQueue(clientAddress, RoutingType.ANYCAST, clientAddress) - } - val sessionFactory = serverLocator.createSessionFactory() - val session = sessionFactory.createSession(rpcUsername, rpcPassword, false, true, true, false, DEFAULT_ACK_BATCH_SIZE) - val consumer = session.createConsumer(clientAddress) - consumer.setMessageHandler(this@RPCClientProxyHandler::artemisMessageHandler) - sessionAndConsumer = ArtemisConsumer(sessionFactory, session, consumer) + sessionFactory = serverLocator.createSessionFactory() + producerSession = sessionFactory!!.createSession(rpcUsername, rpcPassword, false, true, true, false, DEFAULT_ACK_BATCH_SIZE) + rpcProducer = producerSession!!.createProducer(RPCApi.RPC_SERVER_QUEUE_NAME) + consumerSession = sessionFactory!!.createSession(rpcUsername, rpcPassword, false, true, true, false, DEFAULT_ACK_BATCH_SIZE) + consumerSession!!.createTemporaryQueue(clientAddress, RoutingType.ANYCAST, clientAddress) + rpcConsumer = consumerSession!!.createConsumer(clientAddress) + rpcConsumer!!.setMessageHandler(this::artemisMessageHandler) lifeCycle.transition(State.UNSTARTED, State.SERVER_VERSION_NOT_SET) - session.start() + consumerSession!!.start() + producerSession!!.start() } // This is the general function that transforms a client side RPC to internal Artemis messages. @@ -212,7 +207,7 @@ class RPCClientProxyHandler( if (method == toStringMethod) { return "Client RPC proxy for $rpcOpsClass" } - if (sessionAndConsumer!!.session.isClosed) { + if (consumerSession!!.isClosed) { throw RPCException("RPC Proxy is closed") } @@ -220,23 +215,20 @@ class RPCClientProxyHandler( callSiteMap?.set(replyId, Throwable("")) try { val serialisedArguments = (arguments?.toList() ?: emptyList()).serialize(context = serializationContextWithObservableContext) - val request = RPCApi.ClientToServer.RpcRequest(clientAddress, method.name, serialisedArguments.bytes, replyId, sessionId, externalTrace, impersonatedActor) + val request = RPCApi.ClientToServer.RpcRequest( + clientAddress, + method.name, + serialisedArguments, + replyId, + sessionId, + externalTrace, + impersonatedActor + ) val replyFuture = SettableFuture.create() - sessionAndProducerPool.run { - val message = it.session.createMessage(false) - request.writeToClientMessage(message) - - log.debug { - val argumentsString = arguments?.joinToString() ?: "" - "-> RPC(${replyId.value}) -> ${method.name}($argumentsString): ${method.returnType}" - } - - require(rpcReplyMap.put(replyId, replyFuture) == null) { - "Generated several RPC requests with same ID $replyId" - } - it.producer.send(message) - it.session.commit() + require(rpcReplyMap.put(replyId, replyFuture) == null) { + "Generated several RPC requests with same ID $replyId" } + sendMessage(request) return replyFuture.getOrThrow() } catch (e: RuntimeException) { // Already an unchecked exception, so just rethrow it @@ -249,9 +241,24 @@ class RPCClientProxyHandler( } } + private fun sendMessage(message: RPCApi.ClientToServer) { + val artemisMessage = producerSession!!.createMessage(false) + message.writeToClientMessage(artemisMessage) + sendExecutor!!.submit { + artemisMessage.putLongProperty(RPCApi.DEDUPLICATION_SEQUENCE_NUMBER_FIELD_NAME, deduplicationSequenceNumber.getAndIncrement()) + log.debug { "-> RPC -> $message" } + rpcProducer!!.send(artemisMessage) + } + } + // The handler for Artemis messages. private fun artemisMessageHandler(message: ClientMessage) { val serverToClient = RPCApi.ServerToClient.fromClientMessage(serializationContextWithObservableContext, message) + val deduplicationSequenceNumber = message.getLongProperty(RPCApi.DEDUPLICATION_SEQUENCE_NUMBER_FIELD_NAME) + if (deduplicationChecker.checkDuplicateMessageId(serverToClient.deduplicationIdentity, deduplicationSequenceNumber)) { + log.info("Message duplication detected, discarding message") + return + } log.debug { "Got message from RPC server $serverToClient" } when (serverToClient) { is RPCApi.ServerToClient.RpcReply -> { @@ -325,14 +332,12 @@ class RPCClientProxyHandler( * @param notify whether to notify observables or not. */ private fun close(notify: Boolean = true) { - sessionAndConsumer?.sessionFactory?.close() + sessionFactory?.close() reaperScheduledFuture?.cancel(false) observableContext.observableMap.invalidateAll() reapObservables(notify) reaperExecutor?.shutdownNow() - sessionAndProducerPool.close().forEach { - it.sessionFactory.close() - } + sendExecutor?.shutdownNow() // Note the ordering is important, we shut down the consumer *before* the observation executor, otherwise we may // leak borrowed executors. val observationExecutors = observationExecutorPool.close() @@ -385,11 +390,7 @@ class RPCClientProxyHandler( } if (observableIds != null) { log.debug { "Reaping ${observableIds.size} observables" } - sessionAndProducerPool.run { - val message = it.session.createMessage(false) - RPCApi.ClientToServer.ObservablesClosed(observableIds).writeToClientMessage(message) - it.producer.send(message) - } + sendMessage(RPCApi.ClientToServer.ObservablesClosed(observableIds)) } } } diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt index b945487cb9..689674d22f 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt @@ -3,6 +3,7 @@ package net.corda.client.rpc import com.google.common.base.Stopwatch import net.corda.client.rpc.internal.RPCClientConfiguration import net.corda.core.messaging.RPCOps +import net.corda.core.utilities.days import net.corda.core.utilities.minutes import net.corda.core.utilities.seconds import net.corda.node.services.messaging.RPCServerConfiguration @@ -87,13 +88,10 @@ class RPCPerformanceTests : AbstractRPCTest() { val proxy = testProxy( RPCClientConfiguration.default.copy( cacheConcurrencyLevel = 16, - observationExecutorPoolSize = 2, - producerPoolBound = 2 + observationExecutorPoolSize = 2 ), RPCServerConfiguration.default.copy( - rpcThreadPoolSize = 8, - consumerPoolSize = 2, - producerPoolBound = 8 + rpcThreadPoolSize = 8 ) ) @@ -130,13 +128,10 @@ class RPCPerformanceTests : AbstractRPCTest() { val proxy = testProxy( RPCClientConfiguration.default.copy( reapInterval = 1.seconds, - cacheConcurrencyLevel = 16, - producerPoolBound = 8 + cacheConcurrencyLevel = 16 ), RPCServerConfiguration.default.copy( - rpcThreadPoolSize = 8, - consumerPoolSize = 1, - producerPoolBound = 8 + rpcThreadPoolSize = 8 ) ) startPublishingFixedRateInjector( @@ -165,9 +160,7 @@ class RPCPerformanceTests : AbstractRPCTest() { rpcDriver { val proxy = testProxy( RPCClientConfiguration.default, - RPCServerConfiguration.default.copy( - consumerPoolSize = 1 - ) + RPCServerConfiguration.default ) val numberOfMessages = 1000 val bigSize = 10_000_000 diff --git a/confidential-identities/build.gradle b/confidential-identities/build.gradle index c6a29c0d2e..309dce7946 100644 --- a/confidential-identities/build.gradle +++ b/confidential-identities/build.gradle @@ -14,7 +14,7 @@ dependencies { compile project(':core') // Quasar, for suspendable fibres. - compileOnly "co.paralleluniverse:quasar-core:$quasar_version:jdk8" + compileOnly "$quasar_group:quasar-core:$quasar_version:jdk8" testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" testCompile "junit:junit:$junit_version" diff --git a/confidential-identities/src/main/kotlin/net/corda/confidential/IdentitySyncFlow.kt b/confidential-identities/src/main/kotlin/net/corda/confidential/IdentitySyncFlow.kt index b454556bf3..bffff0abf1 100644 --- a/confidential-identities/src/main/kotlin/net/corda/confidential/IdentitySyncFlow.kt +++ b/confidential-identities/src/main/kotlin/net/corda/confidential/IdentitySyncFlow.kt @@ -53,7 +53,7 @@ object IdentitySyncFlow { } private fun extractOurConfidentialIdentities(): Map { - val states: List = (tx.inputs.map { serviceHub.loadState(it) }.requireNoNulls().map { it.data } + tx.outputs.map { it.data }) + val states: List = (serviceHub.loadStates(tx.inputs.toSet()).map { it.state.data } + tx.outputs.map { it.data }) val identities: Set = states.flatMap(ContractState::participants).toSet() // Filter participants down to the set of those not in the network map (are not well known) val confidentialIdentities = identities diff --git a/confidential-identities/src/test/kotlin/net/corda/confidential/IdentitySyncFlowTests.kt b/confidential-identities/src/test/kotlin/net/corda/confidential/IdentitySyncFlowTests.kt index 506f021b68..84b95c31bc 100644 --- a/confidential-identities/src/test/kotlin/net/corda/confidential/IdentitySyncFlowTests.kt +++ b/confidential-identities/src/test/kotlin/net/corda/confidential/IdentitySyncFlowTests.kt @@ -18,7 +18,7 @@ import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.core.CHARLIE_NAME import net.corda.testing.core.singleIdentity -import net.corda.testing.node.MockNetwork +import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.startFlow import org.junit.After import org.junit.Before @@ -28,15 +28,15 @@ import kotlin.test.assertNotNull import kotlin.test.assertNull class IdentitySyncFlowTests { - private lateinit var mockNet: MockNetwork + private lateinit var mockNet: InternalMockNetwork @Before fun before() { // We run this in parallel threads to help catch any race conditions that may exist. - mockNet = MockNetwork( + mockNet = InternalMockNetwork( + cordappPackages = listOf("net.corda.finance.contracts.asset"), networkSendManuallyPumped = false, - threadPerNode = true, - cordappPackages = listOf("net.corda.finance.contracts.asset") + threadPerNode = true ) } diff --git a/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt b/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt index f8fbc6668d..111b8e4ac6 100644 --- a/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt +++ b/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt @@ -3,19 +3,19 @@ package net.corda.confidential import net.corda.core.identity.* import net.corda.core.utilities.getOrThrow import net.corda.testing.core.* -import net.corda.testing.node.MockNetwork +import net.corda.testing.node.internal.InternalMockNetwork import org.junit.Before import net.corda.testing.node.startFlow import org.junit.Test import kotlin.test.* class SwapIdentitiesFlowTests { - private lateinit var mockNet: MockNetwork + private lateinit var mockNet: InternalMockNetwork @Before fun setup() { // We run this in parallel threads to help catch any race conditions that may exist. - mockNet = MockNetwork(emptyList(), networkSendManuallyPumped = false, threadPerNode = true) + mockNet = InternalMockNetwork(emptyList(), networkSendManuallyPumped = false, threadPerNode = true) } @Test diff --git a/constants.properties b/constants.properties index 930a39acd2..c134a442b1 100644 --- a/constants.properties +++ b/constants.properties @@ -1,4 +1,4 @@ -gradlePluginsVersion=3.0.5 +gradlePluginsVersion=4.0.2 kotlinVersion=1.2.20 platformVersion=2 guavaVersion=21.0 @@ -6,4 +6,4 @@ bouncycastleVersion=1.57 typesafeConfigVersion=1.3.1 jsr305Version=3.0.2 artifactoryPluginVersion=4.4.18 -snakeYamlVersion=1.19 \ No newline at end of file +snakeYamlVersion=1.19 diff --git a/core/build.gradle b/core/build.gradle index 5965a1cbc8..f3e24ed0ca 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -75,7 +75,7 @@ dependencies { testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" // Quasar, for suspendable fibres. - compileOnly "co.paralleluniverse:quasar-core:$quasar_version:jdk8" + compileOnly "$quasar_group:quasar-core:$quasar_version:jdk8" // Thread safety annotations compile "com.google.code.findbugs:jsr305:$jsr305_version" diff --git a/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt b/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt index 2f0135ddd1..9d0b745bf7 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt @@ -1,10 +1,14 @@ package net.corda.core.contracts +import net.corda.core.DoNotImplement +import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint.isSatisfiedBy import net.corda.core.crypto.SecureHash +import net.corda.core.internal.AttachmentWithContext 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 +24,20 @@ 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. + */ +object WhitelistedByZoneAttachmentConstraint : AttachmentConstraint { + override fun isSatisfiedBy(attachment: Attachment): Boolean { + return if (attachment is AttachmentWithContext) { + val whitelist = attachment.whitelistedContractImplementations ?: throw IllegalStateException("Unable to verify WhitelistedByZoneAttachmentConstraint - whitelist not specified") + attachment.id in (whitelist[attachment.stateContract] ?: emptyList()) + } 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 diff --git a/core/src/main/kotlin/net/corda/core/contracts/ContractAttachment.kt b/core/src/main/kotlin/net/corda/core/contracts/ContractAttachment.kt index 161d58ea62..e5ab36de93 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/ContractAttachment.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/ContractAttachment.kt @@ -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 = emptySet(), val uploader: String? = null) : Attachment by attachment { + + val allContracts: Set get() = additionalContracts + contract + + override fun toString(): String { + return "ContractAttachment(attachment=${attachment.id}, contracts='${allContracts}', uploader='${uploader}')" + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt b/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt index 26145b38a1..9f706479e1 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt @@ -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) diff --git a/core/src/main/kotlin/net/corda/core/cordapp/ConfigException.kt b/core/src/main/kotlin/net/corda/core/cordapp/ConfigException.kt new file mode 100644 index 0000000000..affbce62dd --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/cordapp/ConfigException.kt @@ -0,0 +1,6 @@ +package net.corda.core.cordapp + +/** + * Thrown if an exception occurs in accessing or parsing cordapp configuration + */ +class CordappConfigException(msg: String, e: Throwable) : Exception(msg, e) \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/cordapp/CordappConfig.kt b/core/src/main/kotlin/net/corda/core/cordapp/CordappConfig.kt new file mode 100644 index 0000000000..664e69fe80 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/cordapp/CordappConfig.kt @@ -0,0 +1,70 @@ +package net.corda.core.cordapp + +import net.corda.core.DoNotImplement + +/** + * Provides access to cordapp configuration independent of the configuration provider. + */ +@DoNotImplement +interface CordappConfig { + /** + * Check if a config exists at path + */ + fun exists(path: String): Boolean + + /** + * Get the value of the configuration at "path". + * + * @throws CordappConfigException If the configuration fails to load, parse, or find a value. + */ + fun get(path: String): Any + + /** + * Get the int value of the configuration at "path". + * + * @throws CordappConfigException If the configuration fails to load, parse, or find a value. + */ + fun getInt(path: String): Int + + /** + * Get the long value of the configuration at "path". + * + * @throws CordappConfigException If the configuration fails to load, parse, or find a value. + */ + fun getLong(path: String): Long + + /** + * Get the float value of the configuration at "path". + * + * @throws CordappConfigException If the configuration fails to load, parse, or find a value. + */ + fun getFloat(path: String): Float + + /** + * Get the double value of the configuration at "path". + * + * @throws CordappConfigException If the configuration fails to load, parse, or find a value. + */ + fun getDouble(path: String): Double + + /** + * Get the number value of the configuration at "path". + * + * @throws CordappConfigException If the configuration fails to load, parse, or find a value. + */ + fun getNumber(path: String): Number + + /** + * Get the string value of the configuration at "path". + * + * @throws CordappConfigException If the configuration fails to load, parse, or find a value. + */ + fun getString(path: String): String + + /** + * Get the boolean value of the configuration at "path". + * + * @throws CordappConfigException If the configuration fails to load, parse, or find a value. + */ + fun getBoolean(path: String): Boolean +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/cordapp/CordappContext.kt b/core/src/main/kotlin/net/corda/core/cordapp/CordappContext.kt index 3c7be4a3e2..b91acec452 100644 --- a/core/src/main/kotlin/net/corda/core/cordapp/CordappContext.kt +++ b/core/src/main/kotlin/net/corda/core/cordapp/CordappContext.kt @@ -2,8 +2,6 @@ package net.corda.core.cordapp import net.corda.core.crypto.SecureHash -// TODO: Add per app config - /** * An app context provides information about where an app was loaded from, access to its classloader, * and (in the included [Cordapp] object) lists of annotated classes discovered via scanning the JAR. @@ -15,5 +13,11 @@ import net.corda.core.crypto.SecureHash * @property attachmentId For CorDapps containing [Contract] or [UpgradedContract] implementations this will be populated * with the attachment containing those class files * @property classLoader the classloader used to load this cordapp's classes + * @property config Configuration for this CorDapp */ -class CordappContext(val cordapp: Cordapp, val attachmentId: SecureHash?, val classLoader: ClassLoader) +class CordappContext internal constructor( + val cordapp: Cordapp, + val attachmentId: SecureHash?, + val classLoader: ClassLoader, + val config: CordappConfig +) diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt index 6b92c533ab..c0a369a730 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt @@ -232,7 +232,7 @@ abstract class FlowLogic { * @returns a [Map] containing the objects received, wrapped in an [UntrustworthyData], by the [FlowSession]s who sent them. */ @Suspendable - open fun receiveAll(sessions: Map>): Map> { + open fun receiveAllMap(sessions: Map>): Map> { return stateMachine.receiveAll(sessions, this) } @@ -250,7 +250,7 @@ abstract class FlowLogic { @Suspendable open fun receiveAll(receiveType: Class, sessions: List): List> { enforceNoDuplicates(sessions) - return castMapValuesToKnownType(receiveAll(associateSessionsToReceiveType(receiveType, sessions))) + return castMapValuesToKnownType(receiveAllMap(associateSessionsToReceiveType(receiveType, sessions))) } /** diff --git a/core/src/main/kotlin/net/corda/core/flows/NotarisationRequest.kt b/core/src/main/kotlin/net/corda/core/flows/NotarisationRequest.kt new file mode 100644 index 0000000000..166d38dc68 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/flows/NotarisationRequest.kt @@ -0,0 +1,101 @@ +package net.corda.core.flows + +import net.corda.core.contracts.StateRef +import net.corda.core.crypto.DigitalSignature +import net.corda.core.crypto.SecureHash +import net.corda.core.identity.Party +import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.serialize +import net.corda.core.transactions.CoreTransaction +import net.corda.core.transactions.SignedTransaction +import java.security.InvalidKeyException +import java.security.SignatureException + +/** + * A notarisation request specifies a list of states to consume and the id of the consuming transaction. Its primary + * purpose is for notarisation traceability – a signature over the notarisation request, [NotarisationRequestSignature], + * allows a notary to prove that a certain party requested the consumption of a particular state. + * + * While the signature must be retained, the notarisation request does not need to be transferred or stored anywhere - it + * can be built from a [SignedTransaction] or a [CoreTransaction]. The notary can recompute it from the committed states index. + * + * In case there is a need to prove that a party spent a particular state, the notary will: + * 1) Locate the consuming transaction id in the index, along with all other states consumed in the same transaction. + * 2) Build a [NotarisationRequest]. + * 3) Locate the [NotarisationRequestSignature] for the transaction id. The signature will contain the signing public key. + * 4) Demonstrate the signature verifies against the serialized request. The provided states are always sorted internally, + * to ensure the serialization does not get affected by the order. + */ +@CordaSerializable +class NotarisationRequest(statesToConsume: List, val transactionId: SecureHash) { + companion object { + /** Sorts in ascending order first by transaction hash, then by output index. */ + private val stateRefComparator = compareBy({ it.txhash }, { it.index }) + } + + private val _statesToConsumeSorted = statesToConsume.sortedWith(stateRefComparator) + + /** States this request specifies to be consumed. Sorted to ensure the serialized form does not get affected by the state order. */ + val statesToConsume: List get() = _statesToConsumeSorted // Getter required for AMQP serialization + + /** Verifies the signature against this notarisation request. Checks that the signature is issued by the right party. */ + fun verifySignature(requestSignature: NotarisationRequestSignature, intendedSigner: Party) { + val signature = requestSignature.digitalSignature + if (intendedSigner.owningKey != signature.by) { + val errorMessage = "Expected a signature by ${intendedSigner.owningKey}, but received by ${signature.by}}" + throw NotaryException(NotaryError.RequestSignatureInvalid(IllegalArgumentException(errorMessage))) + } + // TODO: if requestSignature was generated over an old version of NotarisationRequest, we need to be able to + // reserialize it in that version to get the exact same bytes. Modify the serialization logic once that's + // available. + val expectedSignedBytes = this.serialize().bytes + verifyCorrectBytesSigned(signature, expectedSignedBytes) + } + + private fun verifyCorrectBytesSigned(signature: DigitalSignature.WithKey, bytes: ByteArray) { + try { + signature.verify(bytes) + } catch (e: Exception) { + when (e) { + is InvalidKeyException, is SignatureException -> { + val error = NotaryError.RequestSignatureInvalid(e) + throw NotaryException(error) + } + else -> throw e + } + } + } +} + +/** + * A wrapper around a digital signature used for notarisation requests. + * + * The [platformVersion] is required so the notary can verify the signature against the right version of serialized + * bytes of the [NotarisationRequest]. Otherwise, the request may be rejected. + */ +@CordaSerializable +data class NotarisationRequestSignature(val digitalSignature: DigitalSignature.WithKey, val platformVersion: Int) + +/** + * Container for the transaction and notarisation request signature. + * This is the payload that gets sent by a client to a notary service for committing the input states of the [transaction]. + */ +@CordaSerializable +data class NotarisationPayload(val transaction: Any, val requestSignature: NotarisationRequestSignature) { + init { + require(transaction is SignedTransaction || transaction is CoreTransaction) { + "Unsupported transaction type in the notarisation payload: ${transaction.javaClass.simpleName}" + } + } + + /** + * A helper for automatically casting the underlying [transaction] payload to a [SignedTransaction]. + * Should only be used by validating notaries. + */ + val signedTransaction get() = transaction as SignedTransaction + /** + * A helper for automatically casting the underlying [transaction] payload to a [CoreTransaction]. + * Should only be used by non-validating notaries. + */ + val coreTransaction get() = transaction as CoreTransaction +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt b/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt index 8e38471384..78dd436eb6 100644 --- a/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt @@ -9,10 +9,12 @@ import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.keys import net.corda.core.identity.Party import net.corda.core.internal.FetchDataFlow +import net.corda.core.internal.generateSignature import net.corda.core.node.services.NotaryService import net.corda.core.node.services.TrustedAuthorityNotaryService import net.corda.core.node.services.UniquenessProvider import net.corda.core.serialization.CordaSerializable +import net.corda.core.transactions.CoreTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.UntrustworthyData @@ -61,7 +63,7 @@ class NotaryFlow { protected fun checkTransaction(): Party { val notaryParty = stx.notary ?: throw IllegalStateException("Transaction does not specify a Notary") check(serviceHub.networkMapCache.isNotary(notaryParty)) { "$notaryParty is not a notary on the network" } - check(stx.inputs.all { stateRef -> serviceHub.loadState(stateRef).notary == notaryParty }) { + check(serviceHub.loadStates(stx.inputs.toSet()).all { it.state.notary == notaryParty }) { "Input states must have the same Notary" } @@ -73,15 +75,17 @@ class NotaryFlow { return notaryParty } + /** Notarises the transaction with the [notaryParty], obtains the notary's signature(s). */ @Throws(NotaryException::class) @Suspendable protected fun notarise(notaryParty: Party): UntrustworthyData> { return try { val session = initiateFlow(notaryParty) + val requestSignature = NotarisationRequest(stx.inputs, stx.id).generateSignature(serviceHub) if (serviceHub.networkMapCache.isValidatingNotary(notaryParty)) { - sendAndReceiveValidating(session) + sendAndReceiveValidating(session, requestSignature) } else { - sendAndReceiveNonValidating(notaryParty, session) + sendAndReceiveNonValidating(notaryParty, session, requestSignature) } } catch (e: NotaryException) { if (e.error is NotaryError.Conflict) { @@ -92,21 +96,23 @@ class NotaryFlow { } @Suspendable - protected open fun sendAndReceiveValidating(session: FlowSession): UntrustworthyData> { - subFlow(SendTransactionWithRetry(session, stx)) + private fun sendAndReceiveValidating(session: FlowSession, signature: NotarisationRequestSignature): UntrustworthyData> { + val payload = NotarisationPayload(stx, signature) + subFlow(NotarySendTransactionFlow(session, payload)) return session.receive() } @Suspendable - protected open fun sendAndReceiveNonValidating(notaryParty: Party, session: FlowSession): UntrustworthyData> { - val tx: Any = if (stx.isNotaryChangeTransaction()) { + private fun sendAndReceiveNonValidating(notaryParty: Party, session: FlowSession, signature: NotarisationRequestSignature): UntrustworthyData> { + val tx: CoreTransaction = if (stx.isNotaryChangeTransaction()) { stx.notaryChangeTx // Notary change transactions do not support filtering } else { stx.buildFilteredTransaction(Predicate { it is StateRef || it is TimeWindow || it == notaryParty }) } - return session.sendAndReceiveWithRetry(tx) + return session.sendAndReceiveWithRetry(NotarisationPayload(tx, signature)) } + /** Checks that the notary's signature(s) is/are valid. */ protected fun validateResponse(response: UntrustworthyData>, notaryParty: Party): List { return response.unwrap { signatures -> signatures.forEach { validateSignature(it, stx.id, notaryParty) } @@ -118,16 +124,16 @@ class NotaryFlow { check(sig.by in notaryParty.owningKey.keys) { "Invalid signer for the notary result" } sig.verify(txId) } - } - /** - * The [SendTransactionWithRetry] flow is equivalent to [SendTransactionFlow] but using [sendAndReceiveWithRetry] - * instead of [sendAndReceive], [SendTransactionWithRetry] is intended to be use by the notary client only. - */ - private class SendTransactionWithRetry(otherSideSession: FlowSession, stx: SignedTransaction) : SendTransactionFlow(otherSideSession, stx) { - @Suspendable - override fun sendPayloadAndReceiveDataRequest(otherSideSession: FlowSession, payload: Any): UntrustworthyData { - return otherSideSession.sendAndReceiveWithRetry(payload) + /** + * The [NotarySendTransactionFlow] flow is similar to [SendTransactionFlow], but uses [NotarisationPayload] as the + * initial message, and retries message delivery. + */ + private class NotarySendTransactionFlow(otherSide: FlowSession, payload: NotarisationPayload) : DataVendingFlow(otherSide, payload) { + @Suspendable + override fun sendPayloadAndReceiveDataRequest(otherSideSession: FlowSession, payload: Any): UntrustworthyData { + return otherSideSession.sendAndReceiveWithRetry(payload) + } } } @@ -186,10 +192,16 @@ class NotaryFlow { */ data class TransactionParts(val id: SecureHash, val inputs: List, val timestamp: TimeWindow?, val notary: Party?) +/** + * Exception thrown by the notary service if any issues are encountered while trying to commit a transaction. The + * underlying [error] specifies the cause of failure. + */ class NotaryException(val error: NotaryError) : FlowException("Unable to notarise: $error") +/** Specifies the cause for notarisation request failure. */ @CordaSerializable sealed class NotaryError { + /** Occurs when one or more input states of transaction with [txId] have already been consumed by another transaction. */ data class Conflict(val txId: SecureHash, val conflict: SignedData) : NotaryError() { override fun toString() = "One or more input states for transaction $txId have been used in another transaction" } @@ -199,18 +211,27 @@ sealed class NotaryError { override fun toString() = "Current time $currentTime is outside the time bounds specified by the transaction: $txTimeWindow" companion object { - @JvmField @Deprecated("Here only for binary compatibility purposes, do not use.") + @JvmField + @Deprecated("Here only for binary compatibility purposes, do not use.") val INSTANCE = TimeWindowInvalid(Instant.EPOCH, TimeWindow.fromOnly(Instant.EPOCH)) } } + /** Occurs when the provided transaction fails to verify. */ data class TransactionInvalid(val cause: Throwable) : NotaryError() { override fun toString() = cause.toString() } + /** Occurs when the transaction sent for notarisation is assigned to a different notary identity. */ object WrongNotary : NotaryError() - data class General(val cause: String): NotaryError() { - override fun toString() = cause + /** Occurs when the notarisation request signature does not verify for the provided transaction. */ + data class RequestSignatureInvalid(val cause: Throwable) : NotaryError() { + override fun toString() = "Request signature invalid: $cause" + } + + /** Occurs when the notary service encounters an unexpected issue or becomes temporarily unavailable. */ + data class General(val cause: Throwable) : NotaryError() { + override fun toString() = cause.toString() } } diff --git a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt index 9352d3e178..16f402c486 100644 --- a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt @@ -28,7 +28,7 @@ open class SendTransactionFlow(otherSide: FlowSession, stx: SignedTransaction) : */ open class SendStateAndRefFlow(otherSideSession: FlowSession, stateAndRefs: List>) : DataVendingFlow(otherSideSession, stateAndRefs) -sealed class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any) : FlowLogic() { +open class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any) : FlowLogic() { @Suspendable protected open fun sendPayloadAndReceiveDataRequest(otherSideSession: FlowSession, payload: Any) = otherSideSession.sendAndReceive(payload) diff --git a/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt b/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt index d48b98f290..adbc815537 100644 --- a/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt +++ b/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt @@ -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 { diff --git a/core/src/main/kotlin/net/corda/core/internal/AttachmentWithContext.kt b/core/src/main/kotlin/net/corda/core/internal/AttachmentWithContext.kt new file mode 100644 index 0000000000..677ca29a8d --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/internal/AttachmentWithContext.kt @@ -0,0 +1,22 @@ +package net.corda.core.internal + +import net.corda.core.contracts.Attachment +import net.corda.core.contracts.ContractAttachment +import net.corda.core.contracts.ContractClassName +import net.corda.core.node.services.AttachmentId + +/** + * Used only for passing to the Attachment constraint verification. + */ +class AttachmentWithContext( + val contractAttachment: ContractAttachment, + val stateContract: ContractClassName, + /** Required for verifying [WhitelistedByZoneAttachmentConstraint] */ + val whitelistedContractImplementations: Map>? +) : Attachment by contractAttachment { + init { + require(stateContract in contractAttachment.allContracts) { + "This AttachmentWithContext was not initialised properly" + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt b/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt index 28b44dfa4e..867a719949 100644 --- a/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt @@ -147,7 +147,7 @@ class FetchAttachmentsFlow(requests: Set, override fun maybeWriteToDisk(downloaded: List) { for (attachment in downloaded) { - serviceHub.attachments.importAttachment(attachment.open()) + serviceHub.attachments.importAttachment(attachment.open(), "$P2P_UPLOADER:${otherSideSession.counterparty.name}", null) } } diff --git a/core/src/main/kotlin/net/corda/core/internal/GlobalProperties.kt b/core/src/main/kotlin/net/corda/core/internal/GlobalProperties.kt deleted file mode 100644 index bedac0269a..0000000000 --- a/core/src/main/kotlin/net/corda/core/internal/GlobalProperties.kt +++ /dev/null @@ -1,14 +0,0 @@ -package net.corda.core.internal - -import net.corda.core.node.NetworkParameters - -// TODO: This will cause problems when we run tests in parallel, make each node have its own properties. -object GlobalProperties { - private var _networkParameters: NetworkParameters? = null - - var networkParameters: NetworkParameters - get() = checkNotNull(_networkParameters) { "Property 'networkParameters' has not been initialised." } - set(value) { - _networkParameters = value - } -} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt index 18dcced593..ac4dabae8d 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -2,9 +2,16 @@ package net.corda.core.internal +import net.corda.core.cordapp.Cordapp +import net.corda.core.cordapp.CordappConfig +import net.corda.core.cordapp.CordappContext import net.corda.core.cordapp.CordappProvider import net.corda.core.crypto.* +import net.corda.core.flows.NotarisationRequest +import net.corda.core.flows.NotarisationRequestSignature +import net.corda.core.flows.NotaryFlow import net.corda.core.identity.CordaX500Name +import net.corda.core.node.ServiceHub import net.corda.core.node.ServicesForResolution import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializedBytes @@ -140,6 +147,8 @@ inline fun Path.readLines(charset: Charset = UTF_8, block: (Stream) fun Path.readAllLines(charset: Charset = UTF_8): List = Files.readAllLines(this, charset) fun Path.writeLines(lines: Iterable, charset: Charset = UTF_8, vararg options: OpenOption): Path = Files.write(this, lines, charset, *options) +inline fun Path.readObject(): T = readAll().deserialize() + fun InputStream.copyTo(target: Path, vararg options: CopyOption): Long = Files.copy(this, target, *options) fun String.abbreviate(maxWidth: Int): String = if (length <= maxWidth) this else take(maxWidth - 1) + "…" @@ -297,8 +306,8 @@ fun uncheckedCast(obj: T) = obj as U fun Iterable>.toMultiMap(): Map> = 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 */ @@ -370,8 +379,27 @@ inline fun SerializedBytes.sign(signer: (SerializedBytes) -> Dig return SignedData(this, signer(this)) } -inline fun SerializedBytes.sign(keyPair: KeyPair): SignedData { - return SignedData(this, keyPair.sign(this.bytes)) +fun SerializedBytes.sign(keyPair: KeyPair): SignedData = SignedData(this, keyPair.sign(this.bytes)) + +fun ByteBuffer.copyBytes(): ByteArray = ByteArray(remaining()).also { get(it) } + +fun createCordappContext(cordapp: Cordapp, attachmentId: SecureHash?, classLoader: ClassLoader, config: CordappConfig): CordappContext { + return CordappContext(cordapp, attachmentId, classLoader, config) } -fun ByteBuffer.copyBytes() = ByteArray(remaining()).also { get(it) } +/** Verifies that the correct notarisation request was signed by the counterparty. */ +fun NotaryFlow.Service.validateRequest(request: NotarisationRequest, signature: NotarisationRequestSignature) { + val requestingParty = otherSideSession.counterparty + request.verifySignature(signature, requestingParty) + // TODO: persist the signature for traceability. Do we need to persist the request as well? +} + +/** Creates a signature over the notarisation request using the legal identity key. */ +fun NotarisationRequest.generateSignature(serviceHub: ServiceHub): NotarisationRequestSignature { + val serializedRequest = this.serialize().bytes + val signature = with(serviceHub) { + val myLegalIdentity = myInfo.legalIdentitiesAndCerts.first().owningKey + keyManagementService.sign(serializedRequest, myLegalIdentity) + } + return NotarisationRequestSignature(signature, serviceHub.myInfo.platformVersion) +} diff --git a/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt b/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt index cca7c1433b..41091074a0 100644 --- a/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt +++ b/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt @@ -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 @@ -9,11 +10,13 @@ import java.time.Instant * correctly interoperate with each other. * @property minimumPlatformVersion Minimum version of Corda platform that is required for nodes in the network. * @property notaries List of well known and trusted notary identities with information on validation type. - * @property maxMessageSize Maximum P2P message sent over the wire in bytes. + * @property maxMessageSize This is currently ignored. However, it will be wired up in a future release. * @property maxTransactionSize Maximum permitted transaction size in bytes. * @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: */ // 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> ) { init { require(minimumPlatformVersion > 0) { "minimumPlatformVersion must be at least 1" } diff --git a/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt b/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt index 1977b601d9..d24fa67e7a 100644 --- a/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt +++ b/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt @@ -26,7 +26,9 @@ data class NodeInfo(val addresses: List, ) { // TODO We currently don't support multi-IP/multi-identity nodes, we only left slots in the data structures. init { - require(legalIdentitiesAndCerts.isNotEmpty()) { "Node should have at least one legal identity" } + require(addresses.isNotEmpty()) { "Node must have at least one address" } + require(legalIdentitiesAndCerts.isNotEmpty()) { "Node must have at least one legal identity" } + require(platformVersion > 0) { "Platform version must be at least 1" } } @Transient private var _legalIdentities: List? = null diff --git a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt index 1832ac1cb5..80e1065c99 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -2,6 +2,7 @@ package net.corda.core.node import net.corda.core.DoNotImplement import net.corda.core.contracts.* +import net.corda.core.cordapp.CordappContext import net.corda.core.cordapp.CordappProvider import net.corda.core.crypto.Crypto import net.corda.core.crypto.SignableData @@ -25,6 +26,10 @@ interface StateLoader { /** * Given a [StateRef] loads the referenced transaction and looks up the specified output [ContractState]. * + * *WARNING* Do not use this method unless you really only want a single state - any batch loading should + * go through [loadStates] as repeatedly calling [loadState] can lead to repeat deserialsiation work and + * severe performance degradation. + * * @throws TransactionResolutionException if [stateRef] points to a non-existent transaction. */ @Throws(TransactionResolutionException::class) @@ -38,9 +43,7 @@ interface StateLoader { // TODO: future implementation to use a Vault state ref -> contract state BLOB table and perform single query bulk load // as the existing transaction store will become encrypted at some point @Throws(TransactionResolutionException::class) - fun loadStates(stateRefs: Set): Set> { - return stateRefs.map { StateAndRef(loadState(it), it) }.toSet() - } + fun loadStates(stateRefs: Set): Set> } /** @@ -60,6 +63,9 @@ interface ServicesForResolution : StateLoader { /** Provides access to anything relating to cordapps including contract attachment resolution and app context */ val cordappProvider: CordappProvider + + /** Returns the network parameters the node is operating under. */ + val networkParameters: NetworkParameters } /** @@ -369,4 +375,9 @@ interface ServiceHub : ServicesForResolution { * node starts. */ fun registerUnloadHandler(runOnStop: () -> Unit) + + /** + * See [CordappProvider.getAppContext] + */ + fun getAppContext(): CordappContext = cordappProvider.getAppContext() } diff --git a/core/src/main/kotlin/net/corda/core/node/services/AttachmentStorage.kt b/core/src/main/kotlin/net/corda/core/node/services/AttachmentStorage.kt index 9332587f7a..81e657358e 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/AttachmentStorage.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/AttachmentStorage.kt @@ -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 /** diff --git a/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt b/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt index 69ab1a7307..395e5cbccb 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt @@ -67,12 +67,22 @@ interface NetworkMapCacheBase { fun track(): DataFeed, NetworkMapCache.MapChange> /** - * Look up the node info for a legal name. - * Notice that when there are more than one node for a given name (in case of distributed services) first service node - * found will be returned. + * Return a [NodeInfo] which has the given legal name for one of its identities, or null if no such node is found. + * + * @throws IllegalArgumentException If more than one matching node is found, in the case of a distributed service identity + * (such as with a notary cluster). For such a scenerio use [getNodesByLegalName] instead. */ fun getNodeByLegalName(name: CordaX500Name): NodeInfo? + /** + * Return a list of [NodeInfo]s which have the given legal name for one of their identities, or an empty list if no + * such nodes are found. + * + * Normally there is at most one node for a legal name, but for distributed service identities (such as with a notary + * cluster) there can be multiple nodes sharing the same identity. + */ + fun getNodesByLegalName(name: CordaX500Name): List + /** Look up the node info for a host and port. */ fun getNodeByAddress(address: NetworkHostAndPort): NodeInfo? @@ -100,13 +110,6 @@ interface NetworkMapCacheBase { */ fun getNodesByLegalIdentityKey(identityKey: PublicKey): List - /** - * Look up the node information entries for a legal name. - * Note that normally there will be only one node for a legal name, but for clusters of nodes or distributed services there - * can be multiple nodes. - */ - fun getNodesByLegalName(name: CordaX500Name): List - /** Returns information about the party, which may be a specific node or a service */ fun getPartyInfo(party: Party): PartyInfo? diff --git a/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt b/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt index bc77fe9683..68badaa4e7 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt @@ -94,7 +94,7 @@ abstract class TrustedAuthorityNotaryService : NotaryService() { } } catch (e: Exception) { log.error("Internal error", e) - throw NotaryException(NotaryError.General("Service unavailable, please try again later")) + throw NotaryException(NotaryError.General(Exception("Service unavailable, please try again later"))) } } diff --git a/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt b/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt index 25053adb67..07240e9f30 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt @@ -1,9 +1,7 @@ package net.corda.core.node.services import net.corda.core.DoNotImplement -import net.corda.core.contracts.StateRef -import net.corda.core.contracts.TransactionResolutionException -import net.corda.core.contracts.TransactionState +import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.messaging.DataFeed import net.corda.core.node.StateLoader @@ -26,6 +24,15 @@ interface TransactionStorage : StateLoader { return stx.resolveBaseTransaction(this).outputs[stateRef.index] } + @Throws(TransactionResolutionException::class) + override fun loadStates(stateRefs: Set): Set> { + return stateRefs.groupBy { it.txhash }.flatMap { + val stx = getTransaction(it.key) ?: throw TransactionResolutionException(it.key) + val baseTx = stx.resolveBaseTransaction(this) + it.value.map { StateAndRef(baseTx.outputs[it.index], it) } + }.toSet() + } + /** * Get a synchronous Observable of updates. When observations are pushed to the Observer, the vault will already * incorporate the update. diff --git a/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt b/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt index 82ee0d402a..b8e90fc8c2 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt @@ -1,5 +1,6 @@ package net.corda.core.serialization +import net.corda.core.DoNotImplement import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 import net.corda.core.serialization.internal.effectiveSerializationEnv @@ -99,14 +100,22 @@ abstract class SerializationFactory { } } typealias SerializationMagic = ByteSequence +@DoNotImplement +interface SerializationEncoding + /** * Parameters to serialization and deserialization. */ +@DoNotImplement interface SerializationContext { /** * When serializing, use the format this header sequence represents. */ val preferredSerializationVersion: SerializationMagic + /** + * If non-null, apply this encoding (typically compression) when serializing. + */ + val encoding: SerializationEncoding? /** * The class loader to use for deserialization. */ @@ -115,6 +124,10 @@ interface SerializationContext { * A whitelist that contains (mostly for security purposes) which classes can be serialized and deserialized. */ val whitelist: ClassWhitelist + /** + * A whitelist that determines (mostly for security purposes) whether a particular encoding may be used when deserializing. + */ + val encodingWhitelist: EncodingWhitelist /** * A map of any addition properties specific to the particular use case. */ @@ -161,6 +174,11 @@ interface SerializationContext { */ fun withPreferredSerializationVersion(magic: SerializationMagic): SerializationContext + /** + * A shallow copy of this context but with the given (possibly null) encoding. + */ + fun withEncoding(encoding: SerializationEncoding?): SerializationContext + /** * The use case that we are serializing for, since it influences the implementations chosen. */ @@ -232,3 +250,8 @@ class SerializedBytes(bytes: ByteArray) : OpaqueBytes(bytes) { interface ClassWhitelist { fun hasListed(type: Class<*>): Boolean } + +@DoNotImplement +interface EncodingWhitelist { + fun acceptEncoding(encoding: SerializationEncoding): Boolean +} diff --git a/core/src/main/kotlin/net/corda/core/transactions/BaseTransactions.kt b/core/src/main/kotlin/net/corda/core/transactions/BaseTransactions.kt index 2d82c4850b..10031d795e 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/BaseTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/BaseTransactions.kt @@ -3,12 +3,14 @@ package net.corda.core.transactions import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef +import net.corda.core.serialization.CordaSerializable /** * A transaction with the minimal amount of information required to compute the unique transaction [id], and * resolve a [FullTransaction]. This type of transaction, wrapped in [SignedTransaction], gets transferred across the * wire and recorded to storage. */ +@CordaSerializable abstract class CoreTransaction : BaseTransaction() { /** The inputs of this transaction, containing state references only **/ abstract override val inputs: List diff --git a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt index 1881431114..dfda034eec 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -3,9 +3,11 @@ package net.corda.core.transactions import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.identity.Party +import net.corda.core.internal.AttachmentWithContext import net.corda.core.internal.UpgradeCommand import net.corda.core.internal.castIfPossible import net.corda.core.internal.uncheckedCast +import net.corda.core.node.NetworkParameters import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.Try import java.security.PublicKey @@ -27,7 +29,7 @@ import java.util.function.Predicate // currently sends this across to out-of-process verifiers. We'll need to change that first. // DOCSTART 1 @CordaSerializable -data class LedgerTransaction( +data class LedgerTransaction @JvmOverloads constructor( /** The resolved input states which will be consumed/invalidated by the execution of this transaction. */ override val inputs: List>, override val outputs: List>, @@ -39,7 +41,8 @@ data class LedgerTransaction( override val id: SecureHash, override val notary: Party?, val timeWindow: TimeWindow?, - val privacySalt: PrivacySalt + val privacySalt: PrivacySalt, + private val networkParameters: NetworkParameters? = null ) : FullTransaction() { //DOCEND 1 init { @@ -87,17 +90,29 @@ 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() (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() + val constraintAttachment = AttachmentWithContext(contractAttachment, state.contract, networkParameters?.whitelistedContractImplementations) + if (!state.constraint.isSatisfiedBy(constraintAttachment)) { throw TransactionVerificationException.ContractConstraintRejection(id, state.contract) } } @@ -403,5 +418,14 @@ data class LedgerTransaction( * @throws IllegalArgumentException if no item matches the id. */ fun getAttachment(id: SecureHash): Attachment = attachments.first { it.id == id } -} + fun copy(inputs: List>, + outputs: List>, + commands: List>, + attachments: List, + id: SecureHash, + notary: Party?, + timeWindow: TimeWindow?, + privacySalt: PrivacySalt + ) = copy(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, null) +} diff --git a/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt b/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt index 332718fd94..f0a6c7cc18 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt @@ -42,9 +42,7 @@ data class NotaryChangeWireTransaction( fun resolve(services: ServiceHub, sigs: List) = resolve(services as StateLoader, sigs) fun resolve(stateLoader: StateLoader, sigs: List): NotaryChangeLedgerTransaction { - val resolvedInputs = inputs.map { ref -> - stateLoader.loadState(ref).let { StateAndRef(it, ref) } - } + val resolvedInputs = stateLoader.loadStates(inputs.toSet()).toList() return NotaryChangeLedgerTransaction(resolvedInputs, notary, newNotary, id, sigs) } } diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt index 231f563314..3204e15580 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -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>() + /** * 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) + 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 { + 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 } diff --git a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt index d973152262..46deccf2c6 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt @@ -5,7 +5,7 @@ import net.corda.core.contracts.ComponentGroupEnum.* import net.corda.core.crypto.* import net.corda.core.identity.Party import net.corda.core.internal.Emoji -import net.corda.core.internal.GlobalProperties +import net.corda.core.node.NetworkParameters import net.corda.core.node.ServicesForResolution import net.corda.core.node.services.AttachmentId import net.corda.core.serialization.CordaSerializable @@ -85,11 +85,11 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr */ @Throws(AttachmentResolutionException::class, TransactionResolutionException::class) fun toLedgerTransaction(services: ServicesForResolution): LedgerTransaction { - return toLedgerTransaction( + return toLedgerTransactionInternal( resolveIdentity = { services.identityService.partyFromKey(it) }, resolveAttachment = { services.attachments.openAttachment(it) }, resolveStateRef = { services.loadState(it) }, - resolveContractAttachment = { services.cordappProvider.getContractAttachmentID(it.contract) } + networkParameters = services.networkParameters ) } @@ -100,6 +100,7 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr * @throws AttachmentResolutionException if a required attachment was not found using [resolveAttachment]. * @throws TransactionResolutionException if an input was not found not using [resolveStateRef]. */ + @Deprecated("Use toLedgerTransaction(ServicesForTransaction) instead") @Throws(AttachmentResolutionException::class, TransactionResolutionException::class) fun toLedgerTransaction( resolveIdentity: (PublicKey) -> Party?, @@ -107,7 +108,16 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr resolveStateRef: (StateRef) -> TransactionState<*>?, resolveContractAttachment: (TransactionState) -> AttachmentId? ): LedgerTransaction { - // Look up public keys to authenticated identities. This is just a stub placeholder and will all change in future. + return toLedgerTransactionInternal(resolveIdentity, resolveAttachment, resolveStateRef, null) + } + + private fun toLedgerTransactionInternal( + resolveIdentity: (PublicKey) -> Party?, + resolveAttachment: (SecureHash) -> Attachment?, + resolveStateRef: (StateRef) -> TransactionState<*>?, + networkParameters: NetworkParameters? + ): LedgerTransaction { + // 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) @@ -115,25 +125,23 @@ class WireTransaction(componentGroups: List, 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 ltx = LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, notary, timeWindow, privacySalt) - checkTransactionSize(ltx) + val attachments = attachments.map { resolveAttachment(it) ?: throw AttachmentResolutionException(it) } + val ltx = LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, notary, timeWindow, privacySalt, networkParameters) + checkTransactionSize(ltx, networkParameters?.maxTransactionSize ?: 10485760) return ltx } - private fun checkTransactionSize(ltx: LedgerTransaction) { - var remainingTransactionSize = GlobalProperties.networkParameters.maxTransactionSize + private fun checkTransactionSize(ltx: LedgerTransaction, maxTransactionSize: Int) { + var remainingTransactionSize = maxTransactionSize fun minus(size: Int) { - require(remainingTransactionSize > size) { "Transaction exceeded network's maximum transaction size limit : ${GlobalProperties.networkParameters.maxTransactionSize} bytes." } + require(remainingTransactionSize > size) { "Transaction exceeded network's maximum transaction size limit : $maxTransactionSize bytes." } remainingTransactionSize -= size } - // Check attachment size first as they are most likely to go over the limit. - ltx.attachments.forEach { minus(it.size) } + // Check attachments size first as they are most likely to go over the limit. With ContractAttachment instances + // it's likely that the same underlying Attachment CorDapp will occur more than once so we dedup on the attachment id. + ltx.attachments.distinctBy { it.id }.forEach { minus(it.size) } minus(ltx.inputs.serialize().size) minus(ltx.commands.serialize().size) minus(ltx.outputs.serialize().size) @@ -254,19 +262,6 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr return buf.toString() } - private fun findAttachmentContracts(resolvedInputs: List>, - resolveContractAttachment: (TransactionState) -> AttachmentId?, - resolveAttachment: (SecureHash) -> Attachment? - ): List { - 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) diff --git a/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt b/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt index be483b9e0d..be99697d27 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt @@ -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().also { - doReturn(rigorousMock()).whenever(it).cordappProvider + doReturn(rigorousMock().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) diff --git a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt index 6c31273511..608d7c894e 100644 --- a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt @@ -13,7 +13,6 @@ import net.corda.node.services.persistence.NodeAttachmentService import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.core.singleIdentity -import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNodeParameters import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.startFlow diff --git a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt index 1586c9aa07..e2c24061a3 100644 --- a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt @@ -11,12 +11,13 @@ import net.corda.core.identity.groupAbstractPartyByWellKnownParty import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.getOrThrow +import net.corda.node.internal.StartedNode import net.corda.testing.contracts.DummyContract import net.corda.testing.core.* import net.corda.testing.internal.rigorousMock -import net.corda.testing.node.MockNetwork -import net.corda.testing.node.StartedMockNode import net.corda.testing.node.MockServices +import net.corda.testing.node.internal.InternalMockNetwork +import net.corda.testing.node.internal.InternalMockNetwork.MockNode import net.corda.testing.node.startFlow import org.junit.After import org.junit.Before @@ -29,10 +30,10 @@ class CollectSignaturesFlowTests { private val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")) } - private lateinit var mockNet: MockNetwork - private lateinit var aliceNode: StartedMockNode - private lateinit var bobNode: StartedMockNode - private lateinit var charlieNode: StartedMockNode + private lateinit var mockNet: InternalMockNetwork + private lateinit var aliceNode: StartedNode + private lateinit var bobNode: StartedNode + private lateinit var charlieNode: StartedNode private lateinit var alice: Party private lateinit var bob: Party private lateinit var charlie: Party @@ -40,7 +41,7 @@ class CollectSignaturesFlowTests { @Before fun setup() { - mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts")) + mockNet = InternalMockNetwork(cordappPackages = listOf("net.corda.testing.contracts")) aliceNode = mockNet.createPartyNode(ALICE_NAME) bobNode = mockNet.createPartyNode(BOB_NAME) charlieNode = mockNet.createPartyNode(CHARLIE_NAME) diff --git a/core/src/test/kotlin/net/corda/core/flows/FlowTestsUtils.kt b/core/src/test/kotlin/net/corda/core/flows/FlowTestsUtils.kt index 9ea4d254f1..1c5934aadb 100644 --- a/core/src/test/kotlin/net/corda/core/flows/FlowTestsUtils.kt +++ b/core/src/test/kotlin/net/corda/core/flows/FlowTestsUtils.kt @@ -5,7 +5,6 @@ import net.corda.core.utilities.UntrustworthyData import net.corda.core.utilities.unwrap import net.corda.node.internal.InitiatedFlowFactory import net.corda.node.internal.StartedNode -import net.corda.testing.node.StartedMockNode import net.corda.testing.node.internal.InternalMockNetwork import kotlin.reflect.KClass @@ -81,7 +80,7 @@ infix fun KClass.from(session: FlowSession): Pair.receiveAll(session: Pair>, vararg sessions: Pair>): Map> { val allSessions = arrayOf(session, *sessions) allSessions.enforceNoDuplicates() - return receiveAll(mapOf(*allSessions)) + return receiveAllMap(mapOf(*allSessions)) } /** diff --git a/core/src/test/kotlin/net/corda/core/identity/PartyAndCertificateTest.kt b/core/src/test/kotlin/net/corda/core/identity/PartyAndCertificateTest.kt index f89f6a7829..8831e5905e 100644 --- a/core/src/test/kotlin/net/corda/core/identity/PartyAndCertificateTest.kt +++ b/core/src/test/kotlin/net/corda/core/identity/PartyAndCertificateTest.kt @@ -7,9 +7,9 @@ import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.nodeapi.internal.crypto.X509KeyStore import net.corda.nodeapi.internal.crypto.X509Utilities -import net.corda.testing.core.DEV_ROOT_CA import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.getTestPartyAndCertificate +import net.corda.testing.internal.DEV_ROOT_CA import org.assertj.core.api.Assertions.assertThat import org.junit.Rule import org.junit.Test diff --git a/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt b/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt index fa6ab2b0ff..f6d9432f6a 100644 --- a/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt @@ -8,10 +8,11 @@ import net.corda.core.identity.Party import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.sequence +import net.corda.node.internal.StartedNode import net.corda.testing.contracts.DummyContract -import net.corda.testing.node.MockNetwork import net.corda.testing.core.singleIdentity -import net.corda.testing.node.StartedMockNode +import net.corda.testing.node.internal.InternalMockNetwork +import net.corda.testing.node.internal.InternalMockNetwork.MockNode import net.corda.testing.node.startFlow import org.junit.After import org.junit.Before @@ -27,17 +28,17 @@ import kotlin.test.assertNull // DOCSTART 3 class ResolveTransactionsFlowTest { - private lateinit var mockNet: MockNetwork - private lateinit var notaryNode: StartedMockNode - private lateinit var megaCorpNode: StartedMockNode - private lateinit var miniCorpNode: StartedMockNode + private lateinit var mockNet: InternalMockNetwork + private lateinit var notaryNode: StartedNode + private lateinit var megaCorpNode: StartedNode + private lateinit var miniCorpNode: StartedNode private lateinit var megaCorp: Party private lateinit var miniCorp: Party private lateinit var notary: Party @Before fun setup() { - mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts")) + mockNet = InternalMockNetwork(cordappPackages = listOf("net.corda.testing.contracts")) notaryNode = mockNet.defaultNotaryNode megaCorpNode = mockNet.createPartyNode(CordaX500Name("MegaCorp", "London", "GB")) miniCorpNode = mockNet.createPartyNode(CordaX500Name("MiniCorp", "London", "GB")) diff --git a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt index 8551845849..88acc9a8c2 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt @@ -18,7 +18,6 @@ import net.corda.node.services.persistence.NodeAttachmentService import net.corda.nodeapi.internal.persistence.currentDBSession import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME -import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNodeParameters import net.corda.testing.core.singleIdentity import net.corda.testing.node.internal.InternalMockNetwork diff --git a/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt b/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt index fc87fd23ea..f91eb9c344 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt @@ -29,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().also { doReturn(null).whenever(it).partyFromKey(keyPair.public) }, keyPair) @@ -37,7 +37,7 @@ class LedgerTransactionQueryTests { @Before fun setup() { - services.mockCordappProvider.addMockCordapp(DummyContract.PROGRAM_ID, services.attachments) + services.addMockCordapp(DummyContract.PROGRAM_ID) } interface Commands { diff --git a/core/src/test/kotlin/net/corda/core/transactions/TransactionEncumbranceTests.kt b/core/src/test/kotlin/net/corda/core/transactions/TransactionEncumbranceTests.kt index 6b7e98617f..efdfd28256 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/TransactionEncumbranceTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/TransactionEncumbranceTests.kt @@ -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().also { doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY) }) diff --git a/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt b/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt index cd234f1138..cbda60638f 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt @@ -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>() val outputs = listOf(baseOutState, baseOutState.copy(notary = ALICE), baseOutState.copy(notary = BOB)) val commands = emptyList>() - val attachments = listOf(ContractAttachment(rigorousMock(), DummyContract.PROGRAM_ID)) + val attachments = listOf(ContractAttachment(rigorousMock().also { + doReturn(SecureHash.zeroHash).whenever(it).id + }, DummyContract.PROGRAM_ID)) val id = SecureHash.randomSHA256() val timeWindow: TimeWindow? = null val privacySalt: PrivacySalt = PrivacySalt() diff --git a/docs/source/api-contract-constraints.rst b/docs/source/api-contract-constraints.rst index 4a200e1756..8da496e45c 100644 --- a/docs/source/api-contract-constraints.rst +++ b/docs/source/api-contract-constraints.rst @@ -7,55 +7,135 @@ API: Contract Constraints Contract constraints -------------------- -Transaction states specify a constraint over the contract that will be used to verify it. 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. 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 constraints restrict this universe to a subset). -A typical constraint is the hash of the CorDapp JAR that contains the contract and states but will in future releases -include constraints that require specific signers of the JAR, or both the signer and the hash. Constraints can be -specified when constructing a transaction; if unspecified, an automatic constraint is used. +Corda separates verification of states from their definition. Whilst you might have expected the ``ContractState`` +interface to define a verify method, or perhaps to do verification logic in the constructor, instead it is primarily +done by a method on a ``Contract`` class. This is because what we're actually checking is the +validity of a *transaction*, which is more than just whether the individual states are internally consistent. +The transition between two valid states may be invalid, if the rules of the application are not being respected. +For instance, two cash states of $100 and $200 may both be internally valid, but replacing the first with the second +isn't allowed unless you're a cash issuer - otherwise you could print money for free. + +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. + +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 +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. + +There are two ways of handling upgrades to a smart contract in Corda: + +1. *Implicit:* By allowing multiple implementations of the contract ahead of time, using constraints. +2. *Explicit:* By creating a special *contract upgrade transaction* and getting all participants of a state to sign it using the + contract upgrade flows. + +This article focuses on the first approach. To learn about the second please see :doc:`upgrading-cordapps`. + +The advantage of pre-authorising upgrades using constraints is that you don't need the heavyweight process of creating +upgrade transactions for every state on the ledger. The disadvantage is that you place more faith in third parties, +who could potentially change the app in ways you did not expect or agree with. The advantage of using the explicit +upgrade approach is that you can upgrade states regardless of their constraint, including in cases where you didn't +anticipate a need to do so. But it requires everyone to sign, requires everyone to manually authorise the upgrade, +consumes notary and ledger resources, and is just in general more complex. + +How constraints work +-------------------- + +Starting from Corda 3 there are two types of constraint that can be used: hash and zone whitelist. In future +releases a third type will be added, the signature constraint. + +**Hash constraints.** The behaviour provided by public blockchain systems like Bitcoin and Ethereum is that once data is placed on the ledger, +the program that controls it is fixed and cannot be changed. There is no support for upgrades at all. This implements a +form of "code is law", assuming you trust the community of that blockchain to not release a new version of the platform +that invalidates or changes the meaning of your program. + +This is supported by Corda using a hash constraint. This specifies exactly one hash of a CorDapp JAR that contains the +contract and states any consuming transaction is allowed to use. Once such a state is created, other nodes will only +accept a transaction if it uses that exact JAR file as an attachment. By implication, any bugs in the contract code +or state definitions cannot be fixed except by using an explicit upgrade process via ``ContractUpgradeFlow``. + +.. note:: Corda does not support any way to create states that can never be upgraded at all, but the same effect can be + obtained by using a hash constraint and then simply refusing to agree to any explicit upgrades. Hash + constraints put you in control by requiring an explicit agreement to any upgrade. + +**Zone constraints.** Often a hash constraint will be too restrictive. You do want the ability to upgrade an app, +and you don't mind the upgrade taking effect "just in time" when a transaction happens to be required for other business +reasons. In this case you can use a zone constraint. This specifies that the network parameters of a compatibility zone +(see :doc:`network-map`) is expected to contain a map of class name to hashes of JARs that are allowed to provide that +class. The process for upgrading an app then involves asking the zone operator to add the hash of your new JAR to the +parameters file, and trigger the network parameters upgrade process. This involves each node operator running a shell +command to accept the new parameters file and then restarting the node. Node owners who do not restart their node in +time effectively stop being a part of the network. + +**Signature constraints.** These are not yet supported, but once implemented they will allow a state to require a JAR +signed by a specified identity, via the regular Java jarsigner tool. This will be the most flexible type +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. A ``TransactionState`` has a ``constraint`` field that represents that state's attachment constraint. When a party -constructs a ``TransactionState`` without specifying the constraint parameter a default value -(``AutomaticHashConstraint``) is used. This default will be automatically resolved to a specific -``HashAttachmentConstraint`` that contains the hash of the attachment which contains the contract of that -``TransactionState``. This automatic resolution occurs when a ``TransactionBuilder`` is converted to a -``WireTransaction``. This reduces the boilerplate involved in finding a specific hash constraint when building a -transaction. +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 +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. -It is possible to specify the constraint explicitly with any other class that implements the ``AttachmentConstraint`` -interface. To specify a hash manually the ``HashAttachmentConstraint`` can be used and to not provide any constraint -the ``AlwaysAcceptAttachmentConstraint`` can be used - though this is intended for testing only. An example below -shows how to construct a ``TransactionState`` with an explicitly specified hash constraint from within a flow: +Finally, an ``AlwaysAcceptAttachmentConstraint`` can be used which accepts anything, though this is intended for +testing only. + +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 +your constraint type and your transaction would not verify. + +.. warning:: An AlwaysAccept constraint is effectively the same as disabling security for those states entirely. + Nothing stops you using this constraint in production, but that degrades Corda to being effectively a form + of distributed messaging with optional contract logic being useful only to catch mistakes, rather than potentially + malicious action. If you are deploying an app for which malicious actors aren't in your threat model, using an + AlwaysAccept constraint might simplify things operationally. + +An example below shows how to construct a ``TransactionState`` with an explicitly specified hash constraint from within +a flow: .. sourcecode:: java - // Constructing a transaction with a custom hash constraint on a state - TransactionBuilder tx = new TransactionBuilder() + // Constructing a transaction with a custom hash constraint on a state + TransactionBuilder tx = new TransactionBuilder(); - Party notaryParty = ... // a notary party - DummyState contractState = new DummyState() - SecureHash myAttachmentsHash = serviceHub.cordappProvider.getContractAttachmentID(DummyContract.PROGRAM_ID) - TransactionState transactionState = new TransactionState(contractState, DummyContract.Companion.getPROGRAMID(), notaryParty, new AttachmentHashConstraint(myAttachmentsHash)) + Party notaryParty = ... // a notary party + DummyState contractState = new DummyState(); - 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 + SecureHash myAttachmentHash = SecureHash.parse("2b4042aed7e0e39d312c4c477dca1d96ec5a878ddcfd5583251a8367edbd4a5f"); + TransactionState transactionState = new TransactionState(contractState, DummyContract.Companion.getPROGRAMID(), notaryParty, new AttachmentHashConstraint(myAttachmentHash)); -This mechanism exists both for integrity and security reasons. It is important not to verify against the wrong contract, -which could happen if the wrong version of the contract is attached. More importantly when resolving transaction chains -there will, in a future release, be attachments loaded from the network into the attachment sandbox that are used -to verify the transaction chain. Ensuring the attachment used is the correct one ensures that the verification is -tamper-proof by providing a fake contract. + 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. CorDapps as attachments ----------------------- -CorDapp JARs (:doc:`cordapp-overview`) that are installed to the node and contain classes implementing the ``Contract`` +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 @@ -63,28 +143,12 @@ that they were loaded from. This makes it possible to find the attachment for an automatic resolution of attachments is done by the ``TransactionBuilder`` and how, when verifying the constraints and contracts, attachments are associated with their respective contracts. -Implementations of AttachmentConstraint ---------------------------------------- - -There are three implementations of ``AttachmentConstraint`` with more planned in the future. - -``AlwaysAcceptAttachmentConstraint``: Any attachment (except a missing one) will satisfy this constraint. - -``AutomaticHashConstraint``: This will be resolved to a ``HashAttachmentConstraint`` when a ``TransactionBuilder`` is -converted to a ``WireTransaction``. The ``HashAttachmentConstraint`` will include the attachment hash of the CorDapp -that contains the ``ContractState`` on the ``TransactionState.contract`` field. - -``HashAttachmentConstraint``: Will require that the hash of the attachment containing the contract matches the hash -stored in the constraint. - -We plan to add a future ``AttachmentConstraint`` that will only be satisfied by the presence of signatures on the -attachment JAR. This allows for trusting of attachments from trusted entities. - -Limitations ------------ - -An ``AttachmentConstraint`` is verified by running the ``AttachmentConstraint.isSatisfiedBy`` method. When this is called -it is provided only the relevant attachment by the transaction that is verifying it. +.. 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: + (1) it is inefficient, and (2) it means changes to your flows or other parts of the app will be seen by the ledger + as a "new app", which may end up requiring essentially unnecessary upgrade procedures. It's better to split your + 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`. Testing ------- @@ -93,7 +157,8 @@ Since all tests involving transactions now require attachments it is also requir for tests. Unit test environments in JVM ecosystems tend to use class directories rather than JARs, and so CorDapp JARs typically aren't built for testing. Requiring this would add significant complexity to the build systems of Corda and CorDapps, so the test suite has a set of convenient functions to generate CorDapps from package names or -to specify JAR URLs in the case that the CorDapp(s) involved in testing already exist. +to specify JAR URLs in the case that the CorDapp(s) involved in testing already exist. You can also just use +``AlwaysAcceptAttachmentConstraint`` in your tests to disable the constraints mechanism. MockNetwork/MockNode ******************** @@ -102,12 +167,14 @@ The simplest way to ensure that a vanilla instance of a MockNode generates the c ``cordappPackages`` constructor parameter (Kotlin) or the ``setCordappPackages`` method on ``MockNetworkParameters`` (Java) when creating the MockNetwork. This will cause the ``AbstractNode`` to use the named packages as sources for CorDapps. All files within those packages will be zipped into a JAR and added to the attachment store and loaded as CorDapps by the -``CordappLoader``. An example of this usage would be: +``CordappLoader``. + +An example of this usage would be: .. sourcecode:: java class SomeTestClass { - MockNetwork network = null + MockNetwork network = null; @Before void setup() { @@ -117,6 +184,7 @@ within those packages will be zipped into a JAR and added to the attachment stor ... // Your tests go here } + MockServices ************ @@ -127,6 +195,10 @@ to use as CorDapps using the ``cordappPackages`` parameter. MockServices mockServices = new MockServices(Arrays.asList("com.domain.cordapp")) +However - there is an easier way! If your unit tests are in the same package as the contract code itself, then you +can use the no-args constructor of ``MockServices``. The package to be scanned for CorDapps will be the same as the +the package of the class that constructed the object. This is a convenient default. + Driver ****** diff --git a/docs/source/api-flows.rst b/docs/source/api-flows.rst index f42661aa85..780c2572da 100644 --- a/docs/source/api-flows.rst +++ b/docs/source/api-flows.rst @@ -264,6 +264,13 @@ In order to create a communication session between your initiator flow and the r * ``sendAndReceive(receiveType: Class, payload: Any): R`` * Sends the ``payload`` object and receives an object of type ``receiveType`` back +In addition ``FlowLogic`` provides functions that batch receives: +* ``receiveAllMap(sessions: Map>): Map>`` + * Receives from all ``FlowSession``s specified in the passed in map. The received types may differ. +* ``receiveAll(receiveType: Class, sessions: List): List>`` + * Receives from all ``FlowSession``s specified in the passed in list. The received types must be the same. + +The batched functions are implemented more efficiently by the flow framework. InitiateFlow ~~~~~~~~~~~~ diff --git a/docs/source/api-testing.rst b/docs/source/api-testing.rst new file mode 100644 index 0000000000..4281d32347 --- /dev/null +++ b/docs/source/api-testing.rst @@ -0,0 +1,467 @@ +.. highlight:: kotlin +.. raw:: html + + + + +API: Testing +============ + +.. contents:: + +Flow testing +------------ + +MockNetwork +^^^^^^^^^^^ + +Flow testing can be fully automated using a ``MockNetwork`` composed of ``StartedMockNode`` nodes. Each +``StartedMockNode`` behaves like a regular Corda node, but its services are either in-memory or mocked out. + +A ``MockNetwork`` is created as follows: + +.. container:: codeset + + .. sourcecode:: kotlin + + class FlowTests { + private lateinit var mockNet: MockNetwork + + @Before + fun setup() { + network = MockNetwork(listOf("my.cordapp.package", "my.other.cordapp.package")) + } + } + + + .. sourcecode:: java + + public class IOUFlowTests { + private MockNetwork network; + + @Before + public void setup() { + network = new MockNetwork(ImmutableList.of("my.cordapp.package", "my.other.cordapp.package")); + } + } + +The ``MockNetwork`` requires at a minimum a list of packages. Each package is packaged into a CorDapp JAR and installed +as a CorDapp on each ``StartedMockNode``. + +Configuring the ``MockNetwork`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``MockNetwork`` is configured automatically. You can tweak its configuration using a ``MockNetworkParameters`` +object, or by using named paramters in Kotlin: + +.. container:: codeset + + .. sourcecode:: kotlin + + val network = MockNetwork( + cordappPackages = listOf("my.cordapp.package", "my.other.cordapp.package"), + // If true then each node will be run in its own thread. This can result in race conditions in your + // code if not carefully written, but is more realistic and may help if you have flows in your app that + // do long blocking operations. + threadPerNode = false, + // The notaries to use on the mock network. By default you get one mock notary and that is usually + // sufficient. + notarySpecs = listOf(MockNetworkNotarySpec(DUMMY_NOTARY_NAME)), + // If true then messages will not be routed from sender to receiver until you use the + // [MockNetwork.runNetwork] method. This is useful for writing single-threaded unit test code that can + // examine the state of the mock network before and after a message is sent, without races and without + // the receiving node immediately sending a response. + networkSendManuallyPumped = false, + // How traffic is allocated in the case where multiple nodes share a single identity, which happens for + // notaries in a cluster. You don't normally ever need to change this: it is mostly useful for testing + // notary implementations. + servicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.Random()) + + val network2 = MockNetwork(listOf("my.cordapp.package", "my.other.cordapp.package"), MockNetworkParameters( + // If true then each node will be run in its own thread. This can result in race conditions in your + // code if not carefully written, but is more realistic and may help if you have flows in your app that + // do long blocking operations. + threadPerNode = false, + // The notaries to use on the mock network. By default you get one mock notary and that is usually + // sufficient. + notarySpecs = listOf(MockNetworkNotarySpec(DUMMY_NOTARY_NAME)), + // If true then messages will not be routed from sender to receiver until you use the + // [MockNetwork.runNetwork] method. This is useful for writing single-threaded unit test code that can + // examine the state of the mock network before and after a message is sent, without races and without + // the receiving node immediately sending a response. + networkSendManuallyPumped = false, + // How traffic is allocated in the case where multiple nodes share a single identity, which happens for + // notaries in a cluster. You don't normally ever need to change this: it is mostly useful for testing + // notary implementations. + servicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.Random()) + ) + + .. sourcecode:: java + + MockNetwork network = MockNetwork(ImmutableList.of("my.cordapp.package", "my.other.cordapp.package"), + new MockNetworkParameters() + // If true then each node will be run in its own thread. This can result in race conditions in + // your code if not carefully written, but is more realistic and may help if you have flows in + // your app that do long blocking operations. + .setThreadPerNode(false) + // The notaries to use on the mock network. By default you get one mock notary and that is + // usually sufficient. + .setNotarySpecs(ImmutableList.of(new MockNetworkNotarySpec(DUMMY_NOTARY_NAME))) + // If true then messages will not be routed from sender to receiver until you use the + // [MockNetwork.runNetwork] method. This is useful for writing single-threaded unit test code + // that can examine the state of the mock network before and after a message is sent, without + // races and without the receiving node immediately sending a response. + .setNetworkSendManuallyPumped(false) + // How traffic is allocated in the case where multiple nodes share a single identity, which + // happens for notaries in a cluster. You don't normally ever need to change this: it is mostly + // useful for testing notary implementations. + .setServicePeerAllocationStrategy(new InMemoryMessagingNetwork.ServicePeerAllocationStrategy.Random())); + +Adding nodes to the network +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Nodes are created on the ``MockNetwork`` using: + +.. container:: codeset + + .. sourcecode:: kotlin + + class FlowTests { + private lateinit var mockNet: MockNetwork + lateinit var nodeA: StartedMockNode + lateinit var nodeB: StartedMockNode + + @Before + fun setup() { + network = MockNetwork(listOf("my.cordapp.package", "my.other.cordapp.package")) + nodeA = network.createPartyNode() + // We can optionally give the node a name. + nodeB = network.createPartyNode(CordaX500Name("Bank B", "London", "GB")) + } + } + + + .. sourcecode:: java + + public class IOUFlowTests { + private MockNetwork network; + private StartedMockNode a; + private StartedMockNode b; + + @Before + public void setup() { + network = new MockNetwork(ImmutableList.of("my.cordapp.package", "my.other.cordapp.package")); + nodeA = network.createPartyNode(null); + // We can optionally give the node a name. + nodeB = network.createPartyNode(new CordaX500Name("Bank B", "London", "GB")); + } + } + +Registering a node's initiated flows +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Regular Corda nodes automatically register any response flows defined in their installed CorDapps. When using a +``MockNetwork``, each ``StartedMockNode`` must manually register any responder flows it wishes to use. + +Responder flows are registered as follows: + +.. container:: codeset + + .. sourcecode:: kotlin + + nodeA.registerInitiatedFlow(ExampleFlow.Acceptor::class.java) + + .. sourcecode:: java + + nodeA.registerInitiatedFlow(ExampleFlow.Acceptor.class); + +Running the network +^^^^^^^^^^^^^^^^^^^ + +Regular Corda nodes automatically process received messages. When using a ``MockNetwork`` with +``networkSendManuallyPumped`` set to ``false``, you must manually initiate the processing of received messages. + +You manually process received messages as follows: + +* ``StartedMockNode.pumpReceive`` to process a single message from the node's queue + +* ``MockNetwork.runNetwork`` to process all the messages in every node's queue. This may generate additional messages + that must in turn be processed + + * ``network.runNetwork(-1)`` (the default in Kotlin) will exchange messages until there are no further messages to + process + +Running flows +^^^^^^^^^^^^^ + +A ``StartedMockNode`` starts a flow using the ``StartedNodeServices.startFlow`` method. This method returns a future +representing the output of running the flow. + +.. container:: codeset + + .. sourcecode:: kotlin + + val signedTransactionFuture = nodeA.services.startFlow(IOUFlow(iouValue = 99, otherParty = nodeBParty)) + + .. sourcecode:: java + + CordaFuture future = startFlow(a.getServices(), new ExampleFlow.Initiator(1, nodeBParty)); + +The network must then be manually run before retrieving the future's value: + +.. container:: codeset + + .. sourcecode:: kotlin + + val signedTransactionFuture = nodeA.services.startFlow(IOUFlow(iouValue = 99, otherParty = nodeBParty)) + // Assuming network.networkSendManuallyPumped == false. + network.runNetwork() + val signedTransaction = future.get(); + + .. sourcecode:: java + + CordaFuture future = startFlow(a.getServices(), new ExampleFlow.Initiator(1, nodeBParty)); + // Assuming network.networkSendManuallyPumped == false. + network.runNetwork(); + SignedTransaction signedTransaction = future.get(); + +Accessing ``StartedMockNode`` internals +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Creating a node database transaction +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Whenever you query a node's database (e.g. to extract information from the node's vault), you must wrap the query in +a database transaction, as follows: + +.. container:: codeset + + .. sourcecode:: kotlin + + nodeA.database.transaction { + // Perform query here. + } + + .. sourcecode:: java + + node.getDatabase().transaction(tx -> { + // Perform query here. + } + +Querying a node's vault +~~~~~~~~~~~~~~~~~~~~~~~ + +Recorded states can be retrieved from the vault of a ``StartedMockNode`` using: + +.. container:: codeset + + .. sourcecode:: kotlin + + nodeA.database.transaction { + val myStates = nodeA.services.vaultService.queryBy().states + } + + .. sourcecode:: java + + node.getDatabase().transaction(tx -> { + List myStates = node.getServices().getVaultService().queryBy(MyStateType.class).getStates(); + } + +This allows you to check whether a given state has (or has not) been stored, and whether it has the correct attributes. + + +Examining a node's transaction storage +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Recorded transactions can be retrieved from the transaction storage of a ``StartedMockNode`` using: + +.. container:: codeset + + .. sourcecode:: kotlin + + val transaction = nodeA.services.validatedTransactions.getTransaction(transaction.id) + + .. sourcecode:: java + + SignedTransaction transaction = nodeA.getServices().getValidatedTransactions().getTransaction(transaction.getId()) + +This allows you to check whether a given transaction has (or has not) been stored, and whether it has the correct +attributes. + +This allows you to check whether a given state has (or has not) been stored, and whether it has the correct attributes. + +Further examples +^^^^^^^^^^^^^^^^ + +* See the flow testing tutorial :doc:`here ` +* Further examples are available in the Example CorDapp in + `Java `_ and + `Kotlin `_ + +Contract testing +---------------- + +The Corda test framework includes the ability to create a test ledger by calling the ``ledger`` function +on an implementation of the ``ServiceHub`` interface. + +MockServices +^^^^^^^^^^^^ + +A mock implementation of ``ServiceHub`` is provided in ``MockServices``. This is a minimal ``ServiceHub`` that +suffices to test contract logic. It has the ability to insert states into the vault, query the vault, and +construct and check transactions. + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + :language: kotlin + :start-after: DOCSTART 11 + :end-before: DOCEND 11 + :dedent: 4 + + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + :language: java + :start-after: DOCSTART 11 + :end-before: DOCEND 11 + :dedent: 4 + + +Alternatively, there is a helper constructor which just accepts a list of ``TestIdentity``. The first identity provided is +the identity of the node whose ``ServiceHub`` is being mocked, and any subsequent identities are identities that the node +knows about. Only the calling package is scanned for cordapps and a test ``IdentityService`` is created +for you, using all the given identities. + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + :language: kotlin + :start-after: DOCSTART 12 + :end-before: DOCEND 12 + :dedent: 4 + + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + :language: java + :start-after: DOCSTART 12 + :end-before: DOCEND 12 + :dedent: 4 + + +Writing tests using a test ledger +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``ServiceHub.ledger`` extension function allows you to create a test ledger. Within the ledger wrapper you can create +transactions using the ``transaction`` function. Within a transaction you can define the ``input`` and +``output`` states for the transaction, alongside any commands that are being executed, the ``timeWindow`` in which the +transaction has been executed, and any ``attachments``, as shown in this example test: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + :language: kotlin + :start-after: DOCSTART 13 + :end-before: DOCEND 13 + :dedent: 4 + + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + :language: java + :start-after: DOCSTART 13 + :end-before: DOCEND 13 + :dedent: 4 + +Once all the transaction components have been specified, you can run ``verifies()`` to check that the given transaction is valid. + +Checking for failure states +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In order to test for failures, you can use the ``failsWith`` method, or in Kotlin the ``fails with`` helper method, which +assert that the transaction fails with a specific error. If you just want to assert that the transaction has failed without +verifying the message, there is also a ``fails`` method. + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + :language: kotlin + :start-after: DOCSTART 4 + :end-before: DOCEND 4 + :dedent: 4 + + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + :language: java + :start-after: DOCSTART 4 + :end-before: DOCEND 4 + :dedent: 4 + +.. note:: + + The transaction DSL forces the last line of the test to be either a ``verifies`` or ``fails with`` statement. + +Testing multiple scenarios at once +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Within a single transaction block, you can assert several times that the transaction constructed so far either passes or +fails verification. For example, you could test that a contract fails to verify because it has no output states, and then +add the relevant output state and check that the contract verifies successfully, as in the following example: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + :language: kotlin + :start-after: DOCSTART 5 + :end-before: DOCEND 5 + :dedent: 4 + + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + :language: java + :start-after: DOCSTART 5 + :end-before: DOCEND 5 + :dedent: 4 + +You can also use the ``tweak`` function to create a locally scoped transaction that you can make changes to +and then return to the original, unmodified transaction. As in the following example: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + :language: kotlin + :start-after: DOCSTART 7 + :end-before: DOCEND 7 + :dedent: 4 + + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + :language: java + :start-after: DOCSTART 7 + :end-before: DOCEND 7 + :dedent: 4 + + +Chaining transactions +~~~~~~~~~~~~~~~~~~~~~ + +The following example shows that within a ``ledger``, you can create more than one ``transaction`` in order to test chains +of transactions. In addition to ``transaction``, ``unverifiedTransaction`` can be used, as in the example below, to create +transactions on the ledger without verifying them, for pre-populating the ledger with existing data. When chaining transactions, +it is important to note that even though a ``transaction`` ``verifies`` successfully, the overall ledger may not be valid. This can +be verified separately by placing a ``verifies`` or ``fails`` statement within the ``ledger`` block. + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + :language: kotlin + :start-after: DOCSTART 9 + :end-before: DOCEND 9 + :dedent: 4 + + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + :language: java + :start-after: DOCSTART 9 + :end-before: DOCEND 9 + :dedent: 4 + + +Further examples +^^^^^^^^^^^^^^^^ + +* See the flow testing tutorial :doc:`here ` +* Further examples are available in the Example CorDapp in + `Java `_ and + `Kotlin `_ diff --git a/docs/source/api-transactions.rst b/docs/source/api-transactions.rst index 0c02f4b565..888aed09e7 100644 --- a/docs/source/api-transactions.rst +++ b/docs/source/api-transactions.rst @@ -615,6 +615,24 @@ which the signatures are allowed to be missing: :end-before: DOCEND 36 :dedent: 16 +There is also an overload of ``SignedTransaction.verifySignaturesExcept``, which takes a ``Collection`` of the +public keys for which the signatures are allowed to be missing: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 54 + :end-before: DOCEND 54 + :dedent: 8 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 54 + :end-before: DOCEND 54 + :dedent: 16 + + If the transaction is missing any signatures without the corresponding public keys being passed in, a ``SignaturesMissingException`` is thrown. diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 8bac1b49d6..ffe65a63bb 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -7,6 +7,13 @@ from the previous milestone release. UNRELEASED ---------- +* Added ``NetworkMapCache.getNodesByLegalName`` for querying nodes belonging to a distributed service such as a notary cluster + where they all share a common identity. ``NetworkMapCache.getNodeByLegalName`` has been tightened to throw if more than + one node with the legal name is found. + +* Per CorDapp configuration is now exposed. ``CordappContext`` now exposes a ``CordappConfig`` object that is populated + at CorDapp context creation time from a file source during runtime. + * Introduced Flow Draining mode, in which a node continues executing existing flows, but does not start new. This is to support graceful node shutdown/restarts. In particular, when this mode is on, new flows through RPC will be rejected, scheduled flows will be ignored, and initial session messages will not be consumed. This will ensure that the number of checkpoints will strictly diminish with time, allowing for a clean shutdown. @@ -69,7 +76,10 @@ UNRELEASED :doc:`corda-configuration-file` for more details. * Introducing the concept of network parameters which are a set of constants which all nodes on a network must agree on - to correctly interop. + to correctly interop. These can be retrieved from ``ServiceHub.networkParameters``. + + * One of these parameters, ``maxTransactionSize``, limits the size of a transaction, including its attachments, so that + all nodes have sufficient memory to validate transactions. * The set of valid notaries has been moved to the network parameters. Notaries are no longer identified by the CN in their X500 name. @@ -82,9 +92,6 @@ UNRELEASED * Moved ``NodeInfoSchema`` to internal package as the node info's database schema is not part of the public API. This was needed to allow changes to the schema. - * Introduced max transaction size limit on transactions. The max transaction size parameter is set by the compatibility zone - operator. The parameter is distributed to Corda nodes by network map service as part of the ``NetworkParameters``. - * Support for external user credentials data source and password encryption [CORDA-827]. * Exporting additional JMX metrics (artemis, hibernate statistics) and loading Jolokia agent at JVM startup when using @@ -196,9 +203,9 @@ UNRELEASED * The test utils in ``Expect.kt``, ``SerializationTestHelpers.kt``, ``TestConstants.kt`` and ``TestUtils.kt`` have moved from the ``net.corda.testing`` package to the ``net.corda.testing.core`` package, and ``FlowStackSnapshot.kt`` has moved to the - ``net.corda.testing.services`` package. Moving items out of the ``net.corda.testing.*`` package will help make it clearer which - parts of the api are stable. The bash script ``tools\scripts\update-test-packages.sh`` can be used to smooth the upgrade - process for existing projects. + ``net.corda.testing.services`` package. Moving existing classes out of the ``net.corda.testing.*`` package + will help make it clearer which parts of the api are stable. Scripts have been provided to smooth the upgrade + process for existing projects in the ``tools\scripts`` directory of the Corda repo. .. _changelog_v1: diff --git a/docs/source/conf.py b/docs/source/conf.py index b34fa914f9..ebf92265fb 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -46,7 +46,7 @@ master_doc = 'index' # General information about the project. project = u'R3 Corda' -copyright = u'2017, R3 Limited' +copyright = u'2018, R3 Limited' author = u'R3 DLG' # The version info for the project you're documenting, acts as replacement for diff --git a/docs/source/corda-api.rst b/docs/source/corda-api.rst index aa55bfefcf..f0f803a227 100644 --- a/docs/source/corda-api.rst +++ b/docs/source/corda-api.rst @@ -17,6 +17,7 @@ The following are the core APIs that are used in the development of CorDapps: api-service-hub api-rpc api-core-types + api-testing Before reading this page, you should be familiar with the :doc:`key concepts of Corda `. diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index 9dd048a719..3c41f75d5b 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -185,8 +185,8 @@ path to the node's base directory. :port: The port to start SSH server on -:exportJMXTo: If set to ``http``, will enable JMX metrics reporting via the Jolokia HTTP/JSON agent. - Default Jolokia access url is http://127.0.0.1:7005/jolokia/ +:jmxMonitoringHttpPort: If set, will enable JMX metrics reporting via the Jolokia HTTP/JSON agent on the corresponding port. + Default Jolokia access url is http://127.0.0.1:port/jolokia/ :transactionCacheSizeMegaBytes: Optionally specify how much memory should be used for caching of ledger transactions in memory. Otherwise defaults to 8MB plus 5% of all heap memory above 300MB. diff --git a/docs/source/cordapp-build-systems.rst b/docs/source/cordapp-build-systems.rst index 3900680d81..2de5cca3c7 100644 --- a/docs/source/cordapp-build-systems.rst +++ b/docs/source/cordapp-build-systems.rst @@ -159,3 +159,20 @@ Installing the CorDapp JAR At runtime, nodes will load any CorDapps present in their ``cordapps`` folder. Therefore in order to install a CorDapp on a node, the CorDapp JAR must be added to the ``/cordapps/`` folder, where ``node_dir`` is the folder in which the node's JAR and configuration files are stored. + +CorDapp configuration files +--------------------------- + +CorDapp configuration files should be placed in ``/cordapps/config``. The name of the file should match the +name of the JAR of the CorDapp (eg; if your CorDapp is called ``hello-0.1.jar`` the config should be ``config/hello-0.1.conf``). + +Config files are currently only available in the `Typesafe/Lightbend `_ config format. +These files are loaded when a CorDapp context is created and so can change during runtime. + +CorDapp configuration can be accessed from ``CordappContext::config`` whenever a ``CordappContext`` is available. + +There is an example project that demonstrates in ``samples` called ``cordapp-configuration`` and API documentation in +`_. + + + diff --git a/docs/source/deploying-a-node.rst b/docs/source/deploying-a-node.rst index 6b17b9dd5d..bfc3eaa129 100644 --- a/docs/source/deploying-a-node.rst +++ b/docs/source/deploying-a-node.rst @@ -30,8 +30,8 @@ handling, and ensures the Corda service is run at boot. 4. (Optional) Download the `Corda webserver jar `_ (under ``/VERSION_NUMBER/corda-VERSION_NUMBER.jar``) and place it in ``/opt/corda`` -5. Create a directory called ``plugins`` in ``/opt/corda`` and save your CorDapp jar file to it. Alternatively, download one of - our `sample CorDapps `_ to the ``plugins`` directory +5. Create a directory called ``cordapps`` in ``/opt/corda`` and save your CorDapp jar file to it. Alternatively, download one of + our `sample CorDapps `_ to the ``cordapps`` directory 6. Save the below as ``/opt/corda/node.conf``. See :doc:`corda-configuration-file` for a description of these options @@ -199,8 +199,8 @@ at boot, and means the Corda service stays running with no users connected to th mkdir C:\Corda wget http://jcenter.bintray.com/net/corda/corda/VERSION_NUMBER/corda-VERSION_NUMBER.jar -OutFile C:\Corda\corda.jar -2. Create a directory called ``plugins`` in ``/opt/corda`` and save your CorDapp jar file to it. Alternatively, - download one of our `sample CorDapps `_ to the ``plugins`` directory +2. Create a directory called ``cordapps`` in ``C:\Corda\`` and save your CorDapp jar file to it. Alternatively, + download one of our `sample CorDapps `_ to the ``cordapps`` directory 3. Save the below as ``C:\Corda\node.conf``. See :doc:`corda-configuration-file` for a description of these options @@ -267,13 +267,16 @@ at boot, and means the Corda service stays running with no users connected to th * Set the amount of Java heap memory available to this node by modifying the -Xmx argument * Set an informative description -10. Run the batch file by clicking on it or from a command prompt +10. Provision the required certificates to your node. Contact the network permissioning service or see + :doc:`permissioning` -11. Run ``services.msc`` and verify that a service called ``cordanode1`` is present and running +11. Run the batch file by clicking on it or from a command prompt -12. Run ``netstat -ano`` and check for the ports you configured in ``node.conf`` +12. Run ``services.msc`` and verify that a service called ``cordanode1`` is present and running -13. You may need to open the ports on the Windows firewall +13. Run ``netstat -ano`` and check for the ports you configured in ``node.conf`` + + * You may need to open the ports on the Windows firewall Testing your installation ------------------------- @@ -282,4 +285,4 @@ You can verify Corda is running by connecting to your RPC port from another host ``telnet your-hostname.example.com 10002`` If you receive the message "Escape character is ^]", Corda is running and accessible. Press Ctrl-] and Ctrl-D to exit -telnet. \ No newline at end of file +telnet. diff --git a/docs/source/example-code/build.gradle b/docs/source/example-code/build.gradle index 9757d433c7..1b4c0a5a07 100644 --- a/docs/source/example-code/build.gradle +++ b/docs/source/example-code/build.gradle @@ -77,14 +77,20 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { name "O=Notary Service,OU=corda,L=London,C=GB" notary = [validating : true] p2pPort 10002 - rpcPort 10003 + rpcSettings { + address "localhost:10003" + adminAddress "localhost:10013" + } webPort 10004 cordapps = [] } node { name "O=Alice Corp,L=London,C=GB" p2pPort 10005 - rpcPort 10006 + rpcSettings { + address "localhost:10006" + adminAddress "localhost:10016" + } webPort 10007 cordapps = [] rpcUsers = [ diff --git a/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java b/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java index 9a6f6d6e5f..bf2c58b858 100644 --- a/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java +++ b/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java @@ -540,12 +540,21 @@ public class FlowCookbookJava { // DOCEND 35 // If the transaction is only partially signed, we have to pass in - // a list of the public keys corresponding to the missing + // a vararg of the public keys corresponding to the missing // signatures, explicitly telling the system not to check them. // DOCSTART 36 onceSignedTx.verifySignaturesExcept(counterpartyPubKey); // DOCEND 36 + // There is also an overload of ``verifySignaturesExcept`` which accepts + // a ``Collection`` of the public keys corresponding to the missing + // signatures. In the example below, we could also use + // ``Arrays.asList(counterpartyPubKey)`` instead of + // ``Collections.singletonList(counterpartyPubKey)``. + // DOCSTART 54 + onceSignedTx.verifySignaturesExcept(Collections.singletonList(counterpartyPubKey)); + // DOCEND 54 + // We can also choose to only check the signatures that are // present. BE VERY CAREFUL - this function provides no guarantees // that the signatures are correct, or that none are missing. diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt index f5380582de..0528caeaf3 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt @@ -27,6 +27,7 @@ import net.corda.finance.contracts.asset.Cash import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyState import java.security.PublicKey +import java.security.Signature import java.time.Instant // ``InitiatorFlow`` is our first flow, and will communicate with @@ -205,6 +206,19 @@ class InitiatorFlow(val arg1: Boolean, val arg2: Int, private val counterparty: val packet3: UntrustworthyData = regulatorSession.receive() // DOCEND 06 + // We may also batch receives in order to increase performance. This + // ensures that only a single checkpoint is created for all received + // messages. + // Type-safe variant: + val signatures: List> = + receiveAll(Signature::class.java, listOf(counterpartySession, regulatorSession)) + // Dynamic variant: + val messages: Map> = + receiveAllMap(mapOf( + counterpartySession to Boolean::class.java, + regulatorSession to String::class.java + )) + /**----------------------------------- * EXTRACTING STATES FROM THE VAULT * -----------------------------------**/ @@ -522,12 +536,19 @@ class InitiatorFlow(val arg1: Boolean, val arg2: Int, private val counterparty: // DOCEND 35 // If the transaction is only partially signed, we have to pass in - // a list of the public keys corresponding to the missing + // a vararg of the public keys corresponding to the missing // signatures, explicitly telling the system not to check them. // DOCSTART 36 onceSignedTx.verifySignaturesExcept(counterpartyPubKey) // DOCEND 36 + // There is also an overload of ``verifySignaturesExcept`` which accepts + // a ``Collection`` of the public keys corresponding to the missing + // signatures. + // DOCSTART 54 + onceSignedTx.verifySignaturesExcept(listOf(counterpartyPubKey)) + // DOCEND 54 + // We can also choose to only check the signatures that are // present. BE VERY CAREFUL - this function provides no guarantees // that the signatures are correct, or that none are missing. diff --git a/docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java b/docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java index 0e206bf77c..0fe590c86a 100644 --- a/docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java +++ b/docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java @@ -7,39 +7,66 @@ 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 net.corda.core.crypto.Crypto.generateKeyPair; +import static java.util.Collections.singletonList; 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.core.TestConstants.*; import static net.corda.testing.node.MockServicesKt.makeTestIdentityService; 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); - private static final PublicKey BIG_CORP_PUBKEY = generateKeyPair().getPublic(); - private static final TestIdentity BOB = new TestIdentity(BOB_NAME, 80L); - private static final TestIdentity MEGA_CORP = new TestIdentity(new CordaX500Name("MegaCorp", "London", "GB")); + + private static final TestIdentity alice = new TestIdentity(ALICE_NAME, 70L); + private static final TestIdentity bigCorp = new TestIdentity(new CordaX500Name("BigCorp", "New York", "GB")); + private static final TestIdentity bob = new TestIdentity(BOB_NAME, 80L); + private static final TestIdentity megaCorp = 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() { + // DOCSTART 11 + ledgerServices = new MockServices( + // A list of packages to scan for cordapps + singletonList("net.corda.finance.contracts"), + // The identity represented by this set of mock services. Defaults to a test identity. + // You can also use the alternative parameter initialIdentityName which accepts a + // [CordaX500Name] + megaCorp, + // An implementation of [IdentityService], which contains a list of all identities known + // to the node. Use [makeTestIdentityService] which returns an implementation of + // [InMemoryIdentityService] with the given identities + makeTestIdentityService(megaCorp.getIdentity()) + ); + // DOCEND 11 + } + + @SuppressWarnings("unused") + // DOCSTART 12 + private final MockServices simpleLedgerServices = new MockServices( + // This is the identity of the node + megaCorp, + // Other identities the test node knows about + bigCorp, + alice + ); + // DOCEND 12 // DOCSTART 1 private ICommercialPaperState getPaper() { return new JavaCommercialPaper.State( - MEGA_CORP.ref(defaultRef), - MEGA_CORP.getParty(), - issuedBy(DOLLARS(1000), MEGA_CORP.ref(defaultRef)), + megaCorp.ref(defaultRef), + megaCorp.getParty(), + issuedBy(DOLLARS(1000), megaCorp.ref(defaultRef)), TEST_TX_TIME.plus(7, ChronoUnit.DAYS) ); } @@ -69,7 +96,7 @@ public class CommercialPaperTest { ledger(ledgerServices, l -> { l.transaction(tx -> { tx.input(JCP_PROGRAM_ID, inState); - tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Move()); + tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Move()); tx.attachments(JCP_PROGRAM_ID); return tx.verifies(); }); @@ -85,7 +112,7 @@ public class CommercialPaperTest { ledger(ledgerServices, l -> { l.transaction(tx -> { tx.input(JCP_PROGRAM_ID, inState); - tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Move()); + tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Move()); tx.attachments(JCP_PROGRAM_ID); return tx.failsWith("the state is propagated"); }); @@ -96,15 +123,15 @@ public class CommercialPaperTest { // DOCSTART 5 @Test - public void simpleCPMoveSuccess() { + public void simpleCPMoveSuccessAndFailure() { ICommercialPaperState inState = getPaper(); ledger(ledgerServices, l -> { l.transaction(tx -> { tx.input(JCP_PROGRAM_ID, inState); - tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Move()); + tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Move()); tx.attachments(JCP_PROGRAM_ID); tx.failsWith("the state is propagated"); - tx.output(JCP_PROGRAM_ID, "alice's paper", inState.withOwner(ALICE.getParty())); + tx.output(JCP_PROGRAM_ID, "alice's paper", inState.withOwner(alice.getParty())); return tx.verifies(); }); return Unit.INSTANCE; @@ -112,6 +139,24 @@ public class CommercialPaperTest { } // DOCEND 5 + // DOCSTART 13 + @Test + public void simpleCPMoveSuccess() { + ICommercialPaperState inState = getPaper(); + ledger(ledgerServices, l -> { + l.transaction(tx -> { + tx.input(JCP_PROGRAM_ID, inState); + tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Move()); + tx.attachments(JCP_PROGRAM_ID); + tx.timeWindow(TEST_TX_TIME); + tx.output(JCP_PROGRAM_ID, "alice's paper", inState.withOwner(alice.getParty())); + return tx.verifies(); + }); + return Unit.INSTANCE; + }); + } + // DOCEND 13 + // DOCSTART 6 @Test public void simpleIssuanceWithTweak() { @@ -120,11 +165,11 @@ public class CommercialPaperTest { tx.output(JCP_PROGRAM_ID, "paper", getPaper()); // Some CP is issued onto the ledger by MegaCorp. tx.attachments(JCP_PROGRAM_ID); tx.tweak(tw -> { - tw.command(BIG_CORP_PUBKEY, new JavaCommercialPaper.Commands.Issue()); + tw.command(bigCorp.getPublicKey(), new JavaCommercialPaper.Commands.Issue()); tw.timeWindow(TEST_TX_TIME); return tw.failsWith("output states are issued by a command signer"); }); - tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Issue()); + tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Issue()); tx.timeWindow(TEST_TX_TIME); return tx.verifies(); }); @@ -140,11 +185,11 @@ public class CommercialPaperTest { tx.output(JCP_PROGRAM_ID, "paper", getPaper()); // Some CP is issued onto the ledger by MegaCorp. tx.attachments(JCP_PROGRAM_ID); tx.tweak(tw -> { - tw.command(BIG_CORP_PUBKEY, new JavaCommercialPaper.Commands.Issue()); + tw.command(bigCorp.getPublicKey(), new JavaCommercialPaper.Commands.Issue()); tw.timeWindow(TEST_TX_TIME); return tw.failsWith("output states are issued by a command signer"); }); - tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Issue()); + tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Issue()); tx.timeWindow(TEST_TX_TIME); return tx.verifies(); }); @@ -154,11 +199,11 @@ public class CommercialPaperTest { // DOCSTART 8 @Test public void chainCommercialPaper() { - PartyAndReference issuer = MEGA_CORP.ref(defaultRef); + PartyAndReference issuer = megaCorp.ref(defaultRef); ledger(ledgerServices, l -> { l.unverifiedTransaction(tx -> { tx.output(Cash.PROGRAM_ID, "alice's $900", - new Cash.State(issuedBy(DOLLARS(900), issuer), ALICE.getParty())); + new Cash.State(issuedBy(DOLLARS(900), issuer), alice.getParty())); tx.attachments(Cash.PROGRAM_ID); return Unit.INSTANCE; }); @@ -166,7 +211,7 @@ public class CommercialPaperTest { // Some CP is issued onto the ledger by MegaCorp. l.transaction("Issuance", tx -> { tx.output(JCP_PROGRAM_ID, "paper", getPaper()); - tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Issue()); + tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Issue()); tx.attachments(JCP_PROGRAM_ID); tx.timeWindow(TEST_TX_TIME); return tx.verifies(); @@ -175,11 +220,11 @@ public class CommercialPaperTest { l.transaction("Trade", tx -> { tx.input("paper"); tx.input("alice's $900"); - tx.output(Cash.PROGRAM_ID, "borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), MEGA_CORP.getParty())); + tx.output(Cash.PROGRAM_ID, "borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), megaCorp.getParty())); JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper"); - tx.output(JCP_PROGRAM_ID, "alice's paper", inputPaper.withOwner(ALICE.getParty())); - tx.command(ALICE.getPublicKey(), new Cash.Commands.Move()); - tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Move()); + tx.output(JCP_PROGRAM_ID, "alice's paper", inputPaper.withOwner(alice.getParty())); + tx.command(alice.getPublicKey(), new Cash.Commands.Move()); + tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Move()); return tx.verifies(); }); return Unit.INSTANCE; @@ -190,11 +235,11 @@ public class CommercialPaperTest { // DOCSTART 9 @Test public void chainCommercialPaperDoubleSpend() { - PartyAndReference issuer = MEGA_CORP.ref(defaultRef); + PartyAndReference issuer = megaCorp.ref(defaultRef); ledger(ledgerServices, l -> { l.unverifiedTransaction(tx -> { tx.output(Cash.PROGRAM_ID, "alice's $900", - new Cash.State(issuedBy(DOLLARS(900), issuer), ALICE.getParty())); + new Cash.State(issuedBy(DOLLARS(900), issuer), alice.getParty())); tx.attachments(Cash.PROGRAM_ID); return Unit.INSTANCE; }); @@ -202,7 +247,7 @@ public class CommercialPaperTest { // Some CP is issued onto the ledger by MegaCorp. l.transaction("Issuance", tx -> { tx.output(Cash.PROGRAM_ID, "paper", getPaper()); - tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Issue()); + tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Issue()); tx.attachments(JCP_PROGRAM_ID); tx.timeWindow(TEST_TX_TIME); return tx.verifies(); @@ -211,11 +256,11 @@ public class CommercialPaperTest { l.transaction("Trade", tx -> { tx.input("paper"); tx.input("alice's $900"); - tx.output(Cash.PROGRAM_ID, "borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), MEGA_CORP.getParty())); + tx.output(Cash.PROGRAM_ID, "borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), megaCorp.getParty())); JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper"); - tx.output(JCP_PROGRAM_ID, "alice's paper", inputPaper.withOwner(ALICE.getParty())); - tx.command(ALICE.getPublicKey(), new Cash.Commands.Move()); - tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Move()); + tx.output(JCP_PROGRAM_ID, "alice's paper", inputPaper.withOwner(alice.getParty())); + tx.command(alice.getPublicKey(), new Cash.Commands.Move()); + tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Move()); return tx.verifies(); }); @@ -223,8 +268,8 @@ public class CommercialPaperTest { tx.input("paper"); JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper"); // We moved a paper to other pubkey. - tx.output(JCP_PROGRAM_ID, "bob's paper", inputPaper.withOwner(BOB.getParty())); - tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Move()); + tx.output(JCP_PROGRAM_ID, "bob's paper", inputPaper.withOwner(bob.getParty())); + tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Move()); return tx.verifies(); }); l.fails(); @@ -236,11 +281,11 @@ public class CommercialPaperTest { // DOCSTART 10 @Test public void chainCommercialPaperTweak() { - PartyAndReference issuer = MEGA_CORP.ref(defaultRef); + PartyAndReference issuer = megaCorp.ref(defaultRef); ledger(ledgerServices, l -> { l.unverifiedTransaction(tx -> { tx.output(Cash.PROGRAM_ID, "alice's $900", - new Cash.State(issuedBy(DOLLARS(900), issuer), ALICE.getParty())); + new Cash.State(issuedBy(DOLLARS(900), issuer), alice.getParty())); tx.attachments(Cash.PROGRAM_ID); return Unit.INSTANCE; }); @@ -248,7 +293,7 @@ public class CommercialPaperTest { // Some CP is issued onto the ledger by MegaCorp. l.transaction("Issuance", tx -> { tx.output(Cash.PROGRAM_ID, "paper", getPaper()); - tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Issue()); + tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Issue()); tx.attachments(JCP_PROGRAM_ID); tx.timeWindow(TEST_TX_TIME); return tx.verifies(); @@ -257,11 +302,11 @@ public class CommercialPaperTest { l.transaction("Trade", tx -> { tx.input("paper"); tx.input("alice's $900"); - tx.output(Cash.PROGRAM_ID, "borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), MEGA_CORP.getParty())); + tx.output(Cash.PROGRAM_ID, "borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), megaCorp.getParty())); JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper"); - tx.output(JCP_PROGRAM_ID, "alice's paper", inputPaper.withOwner(ALICE.getParty())); - tx.command(ALICE.getPublicKey(), new Cash.Commands.Move()); - tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Move()); + tx.output(JCP_PROGRAM_ID, "alice's paper", inputPaper.withOwner(alice.getParty())); + tx.command(alice.getPublicKey(), new Cash.Commands.Move()); + tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Move()); return tx.verifies(); }); @@ -270,8 +315,8 @@ public class CommercialPaperTest { tx.input("paper"); JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper"); // We moved a paper to another pubkey. - tx.output(JCP_PROGRAM_ID, "bob's paper", inputPaper.withOwner(BOB.getParty())); - tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Move()); + tx.output(JCP_PROGRAM_ID, "bob's paper", inputPaper.withOwner(bob.getParty())); + tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Move()); return tx.verifies(); }); lw.fails(); diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt index 2b194b1e94..6867695965 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt @@ -74,16 +74,14 @@ class CustomVaultQueryTest { private fun getBalances(): Pair>, Map>> { // Print out the balances - val balancesNodesA = - nodeA.database.transaction { - nodeA.services.getCashBalances() - } + val balancesNodesA = nodeA.transaction { + nodeA.services.getCashBalances() + } println("BalanceA\n" + balancesNodesA) - val balancesNodesB = - nodeB.database.transaction { - nodeB.services.getCashBalances() - } + val balancesNodesB = nodeB.transaction { + nodeB.services.getCashBalances() + } println("BalanceB\n" + balancesNodesB) return Pair(balancesNodesA, balancesNodesB) diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt index 0805565e03..b23907c32b 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt @@ -13,6 +13,7 @@ import net.corda.testing.node.StartedMockNode import net.corda.testing.node.startFlow import org.junit.After import org.junit.Before +import org.junit.Ignore import org.junit.Test import kotlin.test.assertEquals @@ -68,13 +69,14 @@ class FxTransactionBuildTutorialTest { doIt.getOrThrow() // Get the balances when the vault updates nodeAVaultUpdate.get() - val balancesA = nodeA.database.transaction { + val balancesA = nodeA.transaction { nodeA.services.getCashBalances() } nodeBVaultUpdate.get() - val balancesB = nodeB.database.transaction { + val balancesB = nodeB.transaction { nodeB.services.getCashBalances() } + println("BalanceA\n" + balancesA) println("BalanceB\n" + balancesB) // Verify the transfers occurred as expected @@ -86,10 +88,10 @@ class FxTransactionBuildTutorialTest { private fun printBalances() { // Print out the balances - nodeA.database.transaction { + nodeA.transaction { println("BalanceA\n" + nodeA.services.getCashBalances()) } - nodeB.database.transaction { + nodeB.transaction { println("BalanceB\n" + nodeB.services.getCashBalances()) } } diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt index 3b3d5c003a..42d7db3d2a 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt @@ -18,6 +18,7 @@ import net.corda.testing.core.* import net.corda.testing.internal.rigorousMock import net.corda.testing.node.MockServices import net.corda.testing.node.ledger +import net.corda.testing.node.makeTestIdentityService import net.corda.testing.node.transaction import org.junit.Rule import org.junit.Test @@ -25,30 +26,46 @@ import org.junit.Test class CommercialPaperTest { private companion object { val alice = TestIdentity(ALICE_NAME, 70) - val BIG_CORP_PUBKEY = generateKeyPair().public - val BOB = TestIdentity(BOB_NAME, 80).party - val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party + val bob = TestIdentity(BOB_NAME, 80) + val bigCorp = TestIdentity((CordaX500Name("BigCorp", "New York", "GB"))) + val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20) val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")) - val ALICE get() = alice.party - val ALICE_PUBKEY get() = alice.publicKey - val MEGA_CORP get() = megaCorp.party - val MEGA_CORP_PUBKEY get() = megaCorp.publicKey } @Rule @JvmField val testSerialization = SerializationEnvironmentRule() - private val ledgerServices = MockServices(emptyList(), MEGA_CORP.name, rigorousMock().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) + // DOCSTART 11 + private val ledgerServices = MockServices( + // A list of packages to scan for cordapps + listOf("net.corda.finance.contracts"), + // The identity represented by this set of mock services. Defaults to a test identity. + // You can also use the alternative parameter initialIdentityName which accepts a + // [CordaX500Name] + megaCorp, + rigorousMock().also { + doReturn(megaCorp.party).whenever(it).partyFromKey(megaCorp.publicKey) + doReturn(null).whenever(it).partyFromKey(bigCorp.publicKey) + doReturn(null).whenever(it).partyFromKey(alice.publicKey) }) + // DOCEND 11 + + // DOCSTART 12 + @Suppress("unused") + private val simpleLedgerServices = MockServices( + // This is the identity of the node + megaCorp, + // Other identities the test node knows about + bigCorp, + alice + ) + // DOCEND 12 // DOCSTART 1 fun getPaper(): ICommercialPaperState = CommercialPaper.State( - issuance = MEGA_CORP.ref(123), - owner = MEGA_CORP, - faceValue = 1000.DOLLARS `issued by` MEGA_CORP.ref(123), + issuance = megaCorp.party.ref(123), + owner = megaCorp.party, + faceValue = 1000.DOLLARS `issued by` megaCorp.party.ref(123), maturityDate = TEST_TX_TIME + 7.days ) // DOCEND 1 @@ -58,7 +75,7 @@ class CommercialPaperTest { @Test(expected = IllegalStateException::class) fun simpleCP() { val inState = getPaper() - ledgerServices.ledger(DUMMY_NOTARY) { + ledgerServices.ledger(dummyNotary.party) { transaction { attachments(CP_PROGRAM_ID) input(CP_PROGRAM_ID, inState) @@ -73,10 +90,10 @@ class CommercialPaperTest { @Test(expected = TransactionVerificationException.ContractRejection::class) fun simpleCPMove() { val inState = getPaper() - ledgerServices.ledger(DUMMY_NOTARY) { + ledgerServices.ledger(dummyNotary.party) { transaction { input(CP_PROGRAM_ID, inState) - command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Move()) + command(megaCorp.publicKey, CommercialPaper.Commands.Move()) attachments(CP_PROGRAM_ID) verifies() } @@ -88,10 +105,10 @@ class CommercialPaperTest { @Test fun simpleCPMoveFails() { val inState = getPaper() - ledgerServices.ledger(DUMMY_NOTARY) { + ledgerServices.ledger(dummyNotary.party) { transaction { input(CP_PROGRAM_ID, inState) - command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Move()) + command(megaCorp.publicKey, CommercialPaper.Commands.Move()) attachments(CP_PROGRAM_ID) `fails with`("the state is propagated") } @@ -101,35 +118,52 @@ class CommercialPaperTest { // DOCSTART 5 @Test - fun simpleCPMoveSuccess() { + fun simpleCPMoveFailureAndSuccess() { val inState = getPaper() - ledgerServices.ledger(DUMMY_NOTARY) { + ledgerServices.ledger(dummyNotary.party) { transaction { input(CP_PROGRAM_ID, inState) - command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Move()) + command(megaCorp.publicKey, CommercialPaper.Commands.Move()) attachments(CP_PROGRAM_ID) `fails with`("the state is propagated") - output(CP_PROGRAM_ID, "alice's paper", inState.withOwner(ALICE)) + output(CP_PROGRAM_ID, "alice's paper", inState.withOwner(alice.party)) verifies() } } } // DOCEND 5 + // DOCSTART 13 + @Test + fun simpleCPMoveSuccess() { + val inState = getPaper() + ledgerServices.ledger(dummyNotary.party) { + transaction { + input(CP_PROGRAM_ID, inState) + command(megaCorp.publicKey, CommercialPaper.Commands.Move()) + attachments(CP_PROGRAM_ID) + timeWindow(TEST_TX_TIME) + output(CP_PROGRAM_ID, "alice's paper", inState.withOwner(alice.party)) + verifies() + } + } + } + // DOCEND 13 + // DOCSTART 6 @Test fun `simple issuance with tweak`() { - ledgerServices.ledger(DUMMY_NOTARY) { + ledgerServices.ledger(dummyNotary.party) { transaction { output(CP_PROGRAM_ID, "paper", getPaper()) // Some CP is issued onto the ledger by MegaCorp. attachments(CP_PROGRAM_ID) tweak { // The wrong pubkey. - command(BIG_CORP_PUBKEY, CommercialPaper.Commands.Issue()) + command(bigCorp.publicKey, CommercialPaper.Commands.Issue()) timeWindow(TEST_TX_TIME) `fails with`("output states are issued by a command signer") } - command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Issue()) + command(megaCorp.publicKey, CommercialPaper.Commands.Issue()) timeWindow(TEST_TX_TIME) verifies() } @@ -140,16 +174,16 @@ class CommercialPaperTest { // DOCSTART 7 @Test fun `simple issuance with tweak and top level transaction`() { - ledgerServices.transaction(DUMMY_NOTARY) { + ledgerServices.transaction(dummyNotary.party) { output(CP_PROGRAM_ID, "paper", getPaper()) // Some CP is issued onto the ledger by MegaCorp. attachments(CP_PROGRAM_ID) tweak { // The wrong pubkey. - command(BIG_CORP_PUBKEY, CommercialPaper.Commands.Issue()) + command(bigCorp.publicKey, CommercialPaper.Commands.Issue()) timeWindow(TEST_TX_TIME) `fails with`("output states are issued by a command signer") } - command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Issue()) + command(megaCorp.publicKey, CommercialPaper.Commands.Issue()) timeWindow(TEST_TX_TIME) verifies() } @@ -159,17 +193,17 @@ class CommercialPaperTest { // DOCSTART 8 @Test fun `chain commercial paper`() { - val issuer = MEGA_CORP.ref(123) - ledgerServices.ledger(DUMMY_NOTARY) { + val issuer = megaCorp.party.ref(123) + ledgerServices.ledger(dummyNotary.party) { unverifiedTransaction { attachments(Cash.PROGRAM_ID) - output(Cash.PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH issuedBy issuer ownedBy ALICE) + output(Cash.PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH issuedBy issuer ownedBy alice.party) } // Some CP is issued onto the ledger by MegaCorp. transaction("Issuance") { output(CP_PROGRAM_ID, "paper", getPaper()) - command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Issue()) + command(megaCorp.publicKey, CommercialPaper.Commands.Issue()) attachments(CP_PROGRAM_ID) timeWindow(TEST_TX_TIME) verifies() @@ -179,10 +213,10 @@ class CommercialPaperTest { transaction("Trade") { input("paper") input("alice's $900") - output(Cash.PROGRAM_ID, "borrowed $900", 900.DOLLARS.CASH issuedBy issuer ownedBy MEGA_CORP) - output(CP_PROGRAM_ID, "alice's paper", "paper".output().withOwner(ALICE)) - command(ALICE_PUBKEY, Cash.Commands.Move()) - command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Move()) + output(Cash.PROGRAM_ID, "borrowed $900", 900.DOLLARS.CASH issuedBy issuer ownedBy megaCorp.party) + output(CP_PROGRAM_ID, "alice's paper", "paper".output().withOwner(alice.party)) + command(alice.publicKey, Cash.Commands.Move()) + command(megaCorp.publicKey, CommercialPaper.Commands.Move()) verifies() } } @@ -192,17 +226,17 @@ class CommercialPaperTest { // DOCSTART 9 @Test fun `chain commercial paper double spend`() { - val issuer = MEGA_CORP.ref(123) - ledgerServices.ledger(DUMMY_NOTARY) { + val issuer = megaCorp.party.ref(123) + ledgerServices.ledger(dummyNotary.party) { unverifiedTransaction { attachments(Cash.PROGRAM_ID) - output(Cash.PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH issuedBy issuer ownedBy ALICE) + output(Cash.PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH issuedBy issuer ownedBy alice.party) } // Some CP is issued onto the ledger by MegaCorp. transaction("Issuance") { output(CP_PROGRAM_ID, "paper", getPaper()) - command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Issue()) + command(megaCorp.publicKey, CommercialPaper.Commands.Issue()) attachments(CP_PROGRAM_ID) timeWindow(TEST_TX_TIME) verifies() @@ -211,18 +245,18 @@ class CommercialPaperTest { transaction("Trade") { input("paper") input("alice's $900") - output(Cash.PROGRAM_ID, "borrowed $900", 900.DOLLARS.CASH issuedBy issuer ownedBy MEGA_CORP) - output(CP_PROGRAM_ID, "alice's paper", "paper".output().withOwner(ALICE)) - command(ALICE_PUBKEY, Cash.Commands.Move()) - command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Move()) + output(Cash.PROGRAM_ID, "borrowed $900", 900.DOLLARS.CASH issuedBy issuer ownedBy megaCorp.party) + output(CP_PROGRAM_ID, "alice's paper", "paper".output().withOwner(alice.party)) + command(alice.publicKey, Cash.Commands.Move()) + command(megaCorp.publicKey, CommercialPaper.Commands.Move()) verifies() } transaction { input("paper") // We moved a paper to another pubkey. - output(CP_PROGRAM_ID, "bob's paper", "paper".output().withOwner(BOB)) - command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Move()) + output(CP_PROGRAM_ID, "bob's paper", "paper".output().withOwner(bob.party)) + command(megaCorp.publicKey, CommercialPaper.Commands.Move()) verifies() } @@ -234,17 +268,17 @@ class CommercialPaperTest { // DOCSTART 10 @Test fun `chain commercial tweak`() { - val issuer = MEGA_CORP.ref(123) - ledgerServices.ledger(DUMMY_NOTARY) { + val issuer = megaCorp.party.ref(123) + ledgerServices.ledger(dummyNotary.party) { unverifiedTransaction { attachments(Cash.PROGRAM_ID) - output(Cash.PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH issuedBy issuer ownedBy ALICE) + output(Cash.PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH issuedBy issuer ownedBy alice.party) } // Some CP is issued onto the ledger by MegaCorp. transaction("Issuance") { output(CP_PROGRAM_ID, "paper", getPaper()) - command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Issue()) + command(megaCorp.publicKey, CommercialPaper.Commands.Issue()) attachments(CP_PROGRAM_ID) timeWindow(TEST_TX_TIME) verifies() @@ -253,10 +287,10 @@ class CommercialPaperTest { transaction("Trade") { input("paper") input("alice's $900") - output(Cash.PROGRAM_ID, "borrowed $900", 900.DOLLARS.CASH issuedBy issuer ownedBy MEGA_CORP) - output(CP_PROGRAM_ID, "alice's paper", "paper".output().withOwner(ALICE)) - command(ALICE_PUBKEY, Cash.Commands.Move()) - command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Move()) + output(Cash.PROGRAM_ID, "borrowed $900", 900.DOLLARS.CASH issuedBy issuer ownedBy megaCorp.party) + output(CP_PROGRAM_ID, "alice's paper", "paper".output().withOwner(alice.party)) + command(alice.publicKey, Cash.Commands.Move()) + command(megaCorp.publicKey, CommercialPaper.Commands.Move()) verifies() } @@ -264,8 +298,8 @@ class CommercialPaperTest { transaction { input("paper") // We moved a paper to another pubkey. - output(CP_PROGRAM_ID, "bob's paper", "paper".output().withOwner(BOB)) - command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Move()) + output(CP_PROGRAM_ID, "bob's paper", "paper".output().withOwner(bob.party)) + command(megaCorp.publicKey, CommercialPaper.Commands.Move()) verifies() } fails() diff --git a/docs/source/network-map.rst b/docs/source/network-map.rst index 7ec1d0e53c..d5eb8f03ff 100644 --- a/docs/source/network-map.rst +++ b/docs/source/network-map.rst @@ -1,14 +1,25 @@ Network Map =========== -The network map is a collection of signed ``NodeInfo`` objects (signed by the node it represents and thus tamper-proof) -forming the set of reachable nodes in a compatibility zone. A node can receive these objects from two sources: +The network map is a collection of signed ``NodeInfo`` objects. Each NodeInfo is signed by the node it represents and +thus cannot be tampered with. It forms the set of reachable nodes in a compatibility zone. A node can receive these +objects from two sources: -1. The HTTP network map service if the ``compatibilityZoneURL`` config key is specified. +1. A network map server that speaks a simple HTTP based protocol. 2. The ``additional-node-infos`` directory within the node's directory. -HTTP network map service ------------------------- +The network map server also distributes the parameters file that define values for various settings that all nodes need +to agree on to remain in sync. + +.. note:: In Corda 3 no implementation of the HTTP network map server is provided. This is because the details of how + a compatibility zone manages its membership (the databases, ticketing workflows, HSM hardware etc) is expected to vary + between operators, so we provide a simple REST based protocol for uploading/downloading NodeInfos and managing + network parameters. A future version of Corda may provide a simple "stub" implementation for running test zones. + In Corda 3 the right way to run a test network is through distribution of the relevant files via your own mechanisms. + We provide a tool to automate the bulk of this task (see below). + +HTTP network map protocol +------------------------- If the node is configured with the ``compatibilityZoneURL`` config then it first uploads its own signed ``NodeInfo`` to the server (and each time it changes on startup) and then proceeds to download the entire network map. The network map @@ -31,6 +42,12 @@ The set of REST end-points for the network map service are as follows. | GET | /network-map/network-parameters/{hash} | Retrieve the signed network parameters (see below). The entire object is signed with the network map certificate which is also attached. | +----------------+-----------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------+ +HTTP is used for the network map service instead of Corda's own AMQP based peer to peer messaging protocol to +enable the server to be placed behind caching content delivery networks like Cloudflare, Akamai, Amazon Cloudfront and so on. +By using industrial HTTP cache networks the map server can be shielded from DoS attacks more effectively. Additionally, +for the case of distributing small files that rarely change, HTTP is a well understood and optimised protocol. Corda's +own protocol is designed for complex multi-way conversations between authenticated identities using signed binary +messages separated into parallel and nested flows, which isn't necessary for network map distribution. The ``additional-node-infos`` directory --------------------------------------- @@ -43,24 +60,39 @@ latest one is taken. On startup the node generates its own signed node info file, filename of the format ``nodeInfo-${hash}``. To create a simple network without the HTTP network map service then simply place this file in the ``additional-node-infos`` directory -of every node that's part of this network. +of every node that's part of this network. For example, a simple way to do this is to use rsync. + +Usually, test networks have a structure that is known ahead of time. For the creation of such networks we provide a +``network-bootstrapper`` tool. This tool pre-generates node configuration directories if given the IP addresses/domain +names of each machine in the network. The generated node directories contain the NodeInfos for every other node on +the network, along with the network parameters file and identity certificates. Generated nodes do not need to all be +online at once - an offline node that isn't being interacted with doesn't impact the network in any way. So a test +cluster generated like this can be sized for the maximum size you may need, and then scaled up and down as necessary. + +More information can be found in :doc:`setting-up-a-corda-network`. Network parameters ------------------ Network parameters are a set of values that every node participating in the zone needs to agree on and use to -correctly interoperate with each other. If the node is using the HTTP network map service then on first startup it will -download the signed network parameters, cache it in a ``network-parameters`` file and apply them on the node. +correctly interoperate with each other. They can be thought of as an encapsulation of all aspects of a Corda deployment +on which reasonable people may disagree. Whilst other blockchain/DLT systems typically require a source code fork to +alter various constants (like the total number of coins in a cryptocurrency, port numbers to use etc), in Corda we +have refactored these sorts of decisions out into a separate file and allow "zone operators" to make decisions about +them. The operator signs a data structure that contains the values and they are distributed along with the network map. +Tools are provided to gain user opt-in consent to a new version of the parameters and ensure everyone switches to them +at the same time. + +If the node is using the HTTP network map service then on first startup it will download the signed network parameters, +cache it in a ``network-parameters`` file and apply them on the node. .. warning:: If the ``network-parameters`` file is changed and no longer matches what the network map service is advertising then the node will automatically shutdown. Resolution to this is to delete the incorrect file and restart the node so that the parameters can be downloaded again. -.. note:: A future release will support the notion of phased rollout of network parameter changes. - If the node isn't using a HTTP network map service then it's expected the signed file is provided by some other means. For such a scenario there is the network bootstrapper tool which in addition to generating the network parameters file -also distributes the node info files to the node directories. More information can be found in :doc:`setting-up-a-corda-network`. +also distributes the node info files to the node directories. The current set of network parameters: @@ -85,16 +117,23 @@ Network parameters update process --------------------------------- In case of the need to change network parameters Corda zone operator will start the update process. There are many reasons -that may lead to this decision: we discovered that some new fields have to be added to enable smooth network interoperability or change -of the existing compatibility constants is required due to upgrade or security reasons. +that may lead to this decision: adding a notary, setting new fields that were added to enable smooth network interoperability, +or a change of the existing compatibility constants is required, for example. -To synchronize all nodes in the compatibility zone to use the new set of the network parameters two RPC methods exist. The process -requires human interaction and approval of the change. +.. note:: A future release may support the notion of phased rollout of network parameter changes. + +To synchronize all nodes in the compatibility zone to use the new set of the network parameters two RPC methods are +provided. The process requires human interaction and approval of the change, so node operators can review the +differences before agreeing to them. When the update is about to happen the network map service starts to advertise the additional information with the usual network map -data. It includes new network parameters hash, description of the change and the update deadline. Node queries network map server -for the new set of parameters and emits ``ParametersUpdateInfo`` via ``CordaRPCOps::networkParametersFeed`` method to inform -node operator about the event. +data. It includes new network parameters hash, description of the change and the update deadline. Nodes query the network map server +for the new set of parameters. + +The fact a new set of parameters is being advertised shows up in the node logs with the message +"Downloaded new network parameters", and programs connected via RPC can receive ``ParametersUpdateInfo`` by using +the ``CordaRPCOps.networkParametersFeed`` method. Typically a zone operator would also email node operators to let them +know about the details of the impending change, along with the justification, how to object, deadlines and so on. .. container:: codeset @@ -103,12 +142,22 @@ node operator about the event. :start-after: DOCSTART 1 :end-before: DOCEND 1 -Node administrator can review the change and decide if is going to accept it. The approval should be done before ``updateDeadline``. -Nodes that don't approve before the deadline will be removed from the network map. -If the network operator starts advertising a different set of new parameters then that new set overrides the previous set. Only the latest update can be accepted. -To send back parameters approval to the zone operator RPC method ``fun acceptNewNetworkParameters(parametersHash: SecureHash)`` -has to be called with ``parametersHash`` from update. Notice that the process cannot be undone. +The node administrator can review the change and decide if they are going to accept it. The approval should be do +before the update Deadline. Nodes that don't approve before the deadline will likely be removed from the network map by +the zone operator, but that is a decision that is left to the operator's discretion. For example the operator might also +choose to change the deadline instead. + +If the network operator starts advertising a different set of new parameters then that new set overrides the previous set. +Only the latest update can be accepted. + +To send back parameters approval to the zone operator, the RPC method ``fun acceptNewNetworkParameters(parametersHash: SecureHash)`` +has to be called with ``parametersHash`` from the update. Note that approval cannot be undone. You can do this via the Corda +shell (see :doc:`shell`): + +``run acceptNewNetworkParameters parametersHash: "ba19fc1b9e9c1c7cbea712efda5f78b53ae4e5d123c89d02c9da44ec50e9c17d"`` + +If the administrator does not accept the update then next time the node polls network map after the deadline, the +advertised network parameters will be the updated ones. The previous set of parameters will no longer be valid. +At this point the node will automatically shutdown and will require the node operator to bring it back again. + -Next time the node polls network map after the deadline the advertised network parameters will be the updated ones. Previous set -of parameters will no longer be valid. At this point the node will automatically shutdown and will require the node operator -to bring it back again. diff --git a/docs/source/node-administration.rst b/docs/source/node-administration.rst index 2fadfccef0..4132ac9385 100644 --- a/docs/source/node-administration.rst +++ b/docs/source/node-administration.rst @@ -106,7 +106,7 @@ Here are a few ways to build dashboards and extract monitoring data for a node: It can bridge any data input to any output using their plugin system, for example, Telegraf can be configured to collect data from Jolokia and write to DataDog web api. -The Node configuration parameter `exportJMXTo` should be set to ``http`` to ensure a Jolokia agent is instrumented with +The Node configuration parameter `jmxMonitoringHttpPort` has to be present in order to ensure a Jolokia agent is instrumented with the JVM run-time. The following JMX statistics are exported: diff --git a/docs/source/serialization.rst b/docs/source/serialization.rst index eed77ecdd6..de984b9b18 100644 --- a/docs/source/serialization.rst +++ b/docs/source/serialization.rst @@ -396,6 +396,51 @@ Providing a public getter, as per the following example, is acceptable: } } +Mismatched Class Properties / Constructor Parameters +```````````````````````````````````````````````````` + +Consider an example where you wish to ensure that a property of class whose type is some form of container is always sorted using some specific criteria yet you wish to maintain the immutability of the class. + +This could be codified as follows: + +.. container:: codeset + + .. sourcecode:: kotlin + + @CordaSerializable + class ConfirmRequest(statesToConsume: List, val transactionId: SecureHash) { + companion object { + private val stateRefComparator = compareBy({ it.txhash }, { it.index }) + } + + private val states = statesToConsume.sortedWith(stateRefComparator) + } + +The intention in the example is to always ensure that the states are stored in a specific order regardless of the ordering +of the list used to initialise instances of the class. This is achieved by using the first constructor parameter as the +basis for a private member. However, because that member is not mentioned in the constructor (whose parameters determine +what is serializable as discussed above) it would not be serialized. In addition, as there is no provided mechanism to retrieve +a value for ``statesToConsume`` we would fail to build a serializer for this Class. + +In this case a secondary constructor annotated with ``@ConstructorForDeserialization`` would not be a valid solution as the +two signatures would be the same. Best practice is thus to provide a getter for the constructor parameter which explicitly +associates it with the actual member variable. + +.. container:: codeset + + .. sourcecode:: kotlin + + @CordaSerializable + class ConfirmRequest(statesToConsume: List, val transactionId: SecureHash) { + companion object { + private val stateRefComparator = compareBy({ it.txhash }, { it.index }) + } + + private val states = statesToConsume.sortedWith(stateRefComparator) + + //Explicit "getter" for a property identified from the constructor parameters + fun getStatesToConsume() = states + } Enums ````` @@ -451,5 +496,3 @@ and a version of the current state of the class instantiated. More detail can be found in :doc:`serialization-default-evolution`. - - diff --git a/docs/source/upgrading-cordapps.rst b/docs/source/upgrading-cordapps.rst index 893884bb78..70c9a03dbe 100644 --- a/docs/source/upgrading-cordapps.rst +++ b/docs/source/upgrading-cordapps.rst @@ -48,9 +48,6 @@ The ``version`` property, which defaults to 1, specifies the flow's version. Thi whenever there is a release of a flow which has changes that are not backwards-compatible. A non-backwards compatible change is one that changes the interface of the flow. -Currently, CorDapp developers have to explicitly write logic to handle these flow version numbers. In the future, -however, the platform will use prescribed rules for handling versions. - What defines the interface of a flow? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The flow interface is defined by the sequence of ``send`` and ``receive`` calls between an ``InitiatingFlow`` and an @@ -103,28 +100,19 @@ following behaviour: How do I upgrade my flows? ~~~~~~~~~~~~~~~~~~~~~~~~~~ -For flag-day upgrades, the process is simple. -Assumptions -^^^^^^^^^^^ - -* All nodes in the business network can be shut down for a period of time -* All nodes retire the old flows and adopt the new flows at the same time - -Process -^^^^^^^ - -1. Update the flow and test the changes. Increment the flow version number in the ``InitiatingFlow`` annotation +1. Update the flow and test the changes. Increment the flow version number in the ``InitiatingFlow`` annotation. 2. Ensure that all versions of the existing flow have finished running and there are no pending ``SchedulableFlows`` on - any of the nodes on the business network -3. Shut down all the nodes -4. Replace the existing CorDapp JAR with the CorDapp JAR containing the new flow -5. Start the nodes + any of the nodes on the business network. This can be done by *draining the node* (see below). +3. Shut down the node. +4. Replace the existing CorDapp JAR with the CorDapp JAR containing the new flow. +5. Start the node. -From this point onwards, all the nodes will be using the updated flows. +If you shut down all nodes and upgrade them all at the same time, any incompatible change can be made. -In situations where some nodes may still be using previous versions of a flow, the updated flows need to be -backwards-compatible. +In situations where some nodes may still be using previous versions of a flow and thus new versions of your flow may +talk to old versions, the updated flows need to be backwards-compatible. This will be the case for almost any real +deployment in which you cannot easily coordinate the rollout of new code across the network. How do I ensure flow backwards-compatibility? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -262,37 +250,57 @@ In code, inlined subflows appear as regular ``FlowLogic`` instances without eith Inlined flows are not versioned, as they inherit the version of their parent ``InitiatingFlow`` or ``InitiatedBy`` flow. -Are there any other considerations? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Suspended flows -^^^^^^^^^^^^^^^ -Currently, serialised flow state machines persisted in the node's database cannot be updated. All flows must finish -before the updated flow classes are added to the node's plugins folder. - -Flows that don't create sessions -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Flows which are not an ``InitiatingFlow`` or ``InitiatedBy`` flow, or inlined subflows that are not called from an ``InitiatingFlow`` or ``InitiatedBy`` flow, can be updated without consideration of backwards-compatibility. Flows of this type include utility flows for querying the vault and flows for reaching out to external systems. +Flow drains +~~~~~~~~~~~ + +A flow *checkpoint* is a serialised snapshot of the flow's stack frames and any objects reachable from the stack. +Checkpoints are saved to the database automatically when a flow suspends or resumes, which typically happens when +sending or receiving messages. A flow may be replayed from the last checkpoint if the node restarts. Automatic +checkpointing is an unusual feature of Corda and significantly helps developers write reliable code that can survive +node restarts and crashes. It also assists with scaling up, as flows that are waiting for a response can be flushed +from memory. + +However, this means that restoring an old checkpoint to a new version of a flow may cause resume failures. For example +if you remove a local variable from a method that previously had one, then the flow engine won't be able to figure out +where to put the stored value of the variable. + +For this reason, in currently released versions of Corda you must *drain the node* before doing an app upgrade that +changes ``@Suspendable`` code. A drain blocks new flows from starting but allows existing flows to finish. Thus once +a drain is complete there should be no outstanding checkpoints or running flows. Upgrading the app will then succeed. + +A node can be drained or undrained via RPC using the ``setFlowsDrainingModeEnabled`` method, and via the shell using +the standard ``run`` command to invoke the RPC. See :doc:`shell` to learn more. + Contract and state versioning ----------------------------- -Contracts and states can be upgraded if and only if all of the state's participants agree to the proposed upgrade. The -following combinations of upgrades are possible: -* A contract is upgraded while the state definition remains the same -* A state is upgraded while the contract stays the same -* The state and the contract are updated simultaneously +There are two types of contract/state upgrade: + +1. *Implicit:* By allowing multiple implementations of the contract ahead of time, using constraints. See :doc:`api-contract-constraints` to learn more. +2. *Explicit:* By creating a special *contract upgrade transaction* and getting all participants of a state to sign it using the + contract upgrade flows. + +This section of the documentation focuses only on the *explicit* type of upgrade. + +In an explicit upgrade contracts and states can be changed in arbitrary ways, if and only if all of the state' +s participants agree to the proposed upgrade. The following combinations of upgrades are possible: + +* A contract is upgraded while the state definition remains the same. +* A state is upgraded while the contract stays the same. +* The state and the contract are updated simultaneously. The procedure for updating a state or a contract using a flag-day approach is quite simple: -* Update and test the state or contract -* Stop all the nodes on the business network -* Produce a new CorDapp JAR file and distribute it to all the relevant parties -* Start all nodes on the network -* Run the contract upgrade authorisation flow for each state that requires updating on every node -* For each state, one node should run the contract upgrade initiation flow +* Update and test the state or contract. +* Produce a new CorDapp JAR file and distribute it to all the relevant parties. +* Each node operator stops their node, replaces the existing JAR with the new one, and restarts. They may wish to do + a node drain first to avoid the definition of states or contracts changing whilst a flow is in progress. +* Run the contract upgrade authorisation flow for each state that requires updating on every node. +* For each state, one node should run the contract upgrade initiation flow, which will contact the rest. Update Process ~~~~~~~~~~~~~~ diff --git a/docs/source/writing-a-cordapp.rst b/docs/source/writing-a-cordapp.rst index dd8d1662d3..d95849b858 100644 --- a/docs/source/writing-a-cordapp.rst +++ b/docs/source/writing-a-cordapp.rst @@ -26,6 +26,8 @@ For testing purposes, CorDapps may also include: In production, a production-ready webserver should be used, and these files should be moved into a different module or project so that they do not bloat the CorDapp at build time. +.. _cordapp-structure: + Structure --------- You should base the structure of your project on the Java or Kotlin templates: diff --git a/experimental/README.md b/experimental/README.md index f58401d525..99a8957223 100644 --- a/experimental/README.md +++ b/experimental/README.md @@ -7,3 +7,6 @@ either be moved into the main modules and go through code review, or be deleted. Code placed here can be committed to directly onto master at any time as long as it doesn't break the build (no compile failures or unit test failures). Any commits here that break the build will simply be rolled back. +To help reduce the build times, unit tests for experimental projects have been disabled and will NOT run alongside +the whole project tests run via Gradle. Add parameter ```experimental.test.enable``` (example command is ```gradlew test -Dexperimental.test.enable``` +to enable those tests. \ No newline at end of file diff --git a/experimental/kryo-hook/build.gradle b/experimental/kryo-hook/build.gradle index cf52f3c9bb..4ba1fe7495 100644 --- a/experimental/kryo-hook/build.gradle +++ b/experimental/kryo-hook/build.gradle @@ -34,7 +34,7 @@ dependencies { compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" compile "javassist:javassist:$javaassist_version" compile "com.esotericsoftware:kryo:4.0.0" - compile "co.paralleluniverse:quasar-core:$quasar_version:jdk8" + compile "$quasar_group:quasar-core:$quasar_version:jdk8" } jar { diff --git a/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt index 9d18c2d3a5..28e98035c7 100644 --- a/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt @@ -97,7 +97,7 @@ object TwoPartyTradeFlow { val signTransactionFlow = object : SignTransactionFlow(otherSideSession, VERIFYING_AND_SIGNING.childProgressTracker()) { override fun checkTransaction(stx: SignedTransaction) { // Verify that we know who all the participants in the transaction are - val states: Iterable = stx.tx.inputs.map { serviceHub.loadState(it).data } + stx.tx.outputs.map { it.data } + val states: Iterable = serviceHub.loadStates(stx.tx.inputs.toSet()).map { it.state.data } + stx.tx.outputs.map { it.data } states.forEach { state -> state.participants.forEach { anon -> require(serviceHub.identityService.wellKnownPartyFromAnonymous(anon) != null) { diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt index 87f481396b..fa32531bfd 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt @@ -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 ) = group.apply { diff --git a/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt b/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt index 2d3b901f9a..d9b74c3cec 100644 --- a/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt @@ -9,10 +9,11 @@ import net.corda.core.utilities.getOrThrow import net.corda.finance.DOLLARS import net.corda.finance.`issued by` import net.corda.finance.contracts.asset.Cash +import net.corda.node.internal.StartedNode import net.corda.testing.core.* import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin -import net.corda.testing.node.MockNetwork -import net.corda.testing.node.StartedMockNode +import net.corda.testing.node.internal.InternalMockNetwork +import net.corda.testing.node.internal.InternalMockNetwork.MockNode import net.corda.testing.node.startFlow import org.junit.After import org.junit.Before @@ -21,16 +22,16 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith class CashPaymentFlowTests { - private lateinit var mockNet: MockNetwork + private lateinit var mockNet: InternalMockNetwork private val initialBalance = 2000.DOLLARS private val ref = OpaqueBytes.of(0x01) - private lateinit var bankOfCordaNode: StartedMockNode + private lateinit var bankOfCordaNode: StartedNode private lateinit var bankOfCorda: Party - private lateinit var aliceNode: StartedMockNode + private lateinit var aliceNode: StartedNode @Before fun start() { - mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin(), cordappPackages = listOf("net.corda.finance.contracts.asset")) + mockNet = InternalMockNetwork(servicePeerAllocationStrategy = RoundRobin(), cordappPackages = listOf("net.corda.finance.contracts.asset")) bankOfCordaNode = mockNet.createPartyNode(BOC_NAME) bankOfCorda = bankOfCordaNode.info.identityFromX500Name(BOC_NAME) aliceNode = mockNet.createPartyNode(ALICE_NAME) diff --git a/gradle-plugins/README.rst b/gradle-plugins/README.rst deleted file mode 100644 index 0ede7c216f..0000000000 --- a/gradle-plugins/README.rst +++ /dev/null @@ -1,29 +0,0 @@ -Gradle Plugins for Cordapps -=========================== - -The projects at this level of the project are gradle plugins for cordapps and are published to Maven Local with -the rest of the Corda libraries. - -.. note:: - - Some of the plugins here are duplicated with the ones in buildSrc. While the duplication is unwanted any - currently known solution (such as publishing from buildSrc or setting up a separate project/repo) would - introduce a two step build which is less convenient. - -Version number --------------- - -To modify the version number edit constants.properties in root dir - -Installing ----------- - -If you need to bootstrap the corda repository you can install these plugins with - -.. code-block:: text - - cd publish-utils - ../../gradlew -u install - cd ../ - ../gradlew install - diff --git a/gradle-plugins/api-scanner/README.md b/gradle-plugins/api-scanner/README.md deleted file mode 100644 index aba34357d4..0000000000 --- a/gradle-plugins/api-scanner/README.md +++ /dev/null @@ -1,79 +0,0 @@ -# API Scanner - -Generates a text summary of Corda's public API that we can check for API-breaking changes. - -```bash -$ gradlew generateApi -``` - -See [here](../../docs/source/corda-api.rst) for Corda's public API strategy. We will need to -apply this plugin to other modules in future Corda releases as those modules' APIs stabilise. - -Basically, this plugin will document a module's `public` and `protected` classes/methods/fields, -excluding those from our `*.internal.*` packages, any synthetic methods, bridge methods, or methods -identified as having Kotlin's `internal` scope. (Kotlin doesn't seem to have implemented `internal` -scope for classes or fields yet as these are currently `public` inside the `.class` file.) - -## Usage -Include this line in the `build.gradle` file of every Corda module that exports public API: - -```gradle -apply plugin: 'net.corda.plugins.api-scanner' -``` - -This will create a Gradle task called `scanApi` which will analyse that module's Jar artifacts. More precisely, -it will analyse all of the Jar artifacts that have not been assigned a Maven classifier, on the basis -that these should be the module's main artifacts. - -The `scanApi` task supports the following configuration options: -```gradle -scanApi { - // Make the classpath-scanning phase more verbose. - verbose = {true|false} - - // Enable / disable the task within this module. - enabled = {true|false} - - // Names of classes that should be excluded from the output. - excludeClasses = [ - ... - ] -} -``` - -All of the `ScanApi` tasks write their output files to their own `$buildDir/api` directory, where they -are collated into a single output file by the `GenerateApi` task. The `GenerateApi` task is declared -in the root project's `build.gradle` file: - -```gradle -task generateApi(type: net.corda.plugins.GenerateApi){ - baseName = "api-corda" -} -``` - -The final API file is written to `$buildDir/api/$baseName-$project.version.txt` - -### Sample Output -``` -public interface net.corda.core.contracts.Attachment extends net.corda.core.contracts.NamedByHash - public abstract void extractFile(String, java.io.OutputStream) - @org.jetbrains.annotations.NotNull public abstract List getSigners() - @org.jetbrains.annotations.NotNull public abstract java.io.InputStream open() - @org.jetbrains.annotations.NotNull public abstract jar.JarInputStream openAsJAR() -## -public interface net.corda.core.contracts.AttachmentConstraint - public abstract boolean isSatisfiedBy(net.corda.core.contracts.Attachment) -## -public final class net.corda.core.contracts.AttachmentResolutionException extends net.corda.core.flows.FlowException - public (net.corda.core.crypto.SecureHash) - @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getHash() -## -``` - -#### Notes -The `GenerateApi` task will collate the output of every `ScanApi` task found either in the same project, -or in any of that project's subprojects. So it is _theoretically_ possible also to collate the API output -from subtrees of modules simply by defining a new `GenerateApi` task at the root of that subtree. - -## Plugin Installation -See [here](../README.rst) for full installation instructions. diff --git a/gradle-plugins/api-scanner/build.gradle b/gradle-plugins/api-scanner/build.gradle deleted file mode 100644 index c178472d58..0000000000 --- a/gradle-plugins/api-scanner/build.gradle +++ /dev/null @@ -1,19 +0,0 @@ -apply plugin: 'java' -apply plugin: 'net.corda.plugins.publish-utils' -apply plugin: 'com.jfrog.artifactory' - -description "Generates a summary of the artifact's public API" - -repositories { - mavenCentral() -} - -dependencies { - compile gradleApi() - compile "io.github.lukehutch:fast-classpath-scanner:2.7.0" - testCompile "junit:junit:4.12" -} - -publish { - name project.name -} diff --git a/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ApiScanner.java b/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ApiScanner.java deleted file mode 100644 index 75f238891c..0000000000 --- a/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ApiScanner.java +++ /dev/null @@ -1,70 +0,0 @@ -package net.corda.plugins; - -import org.gradle.api.Plugin; -import org.gradle.api.Project; -import org.gradle.api.artifacts.ConfigurationContainer; -import org.gradle.api.file.FileCollection; -import org.gradle.api.tasks.TaskCollection; -import org.gradle.jvm.tasks.Jar; - -public class ApiScanner implements Plugin { - - /** - * Identify the Gradle Jar tasks creating jars - * without Maven classifiers, and generate API - * documentation for them. - * @param p Current project. - */ - @Override - public void apply(Project p) { - p.getLogger().info("Applying API scanner to {}", p.getName()); - - ScannerExtension extension = p.getExtensions().create("scanApi", ScannerExtension.class); - - p.afterEvaluate(project -> { - TaskCollection jarTasks = project.getTasks() - .withType(Jar.class) - .matching(jarTask -> jarTask.getClassifier().isEmpty() && jarTask.isEnabled()); - if (jarTasks.isEmpty()) { - return; - } - - project.getLogger().info("Adding scanApi task to {}", project.getName()); - project.getTasks().create("scanApi", ScanApi.class, scanTask -> { - scanTask.setClasspath(compilationClasspath(project.getConfigurations())); - // Automatically creates a dependency on jar tasks. - scanTask.setSources(project.files(jarTasks)); - scanTask.setExcludeClasses(extension.getExcludeClasses()); - scanTask.setVerbose(extension.isVerbose()); - scanTask.setEnabled(extension.isEnabled()); - - // Declare this ScanApi task to be a dependency of any - // GenerateApi tasks belonging to any of our ancestors. - project.getRootProject().getTasks() - .withType(GenerateApi.class) - .matching(generateTask -> isAncestorOf(generateTask.getProject(), project)) - .forEach(generateTask -> generateTask.dependsOn(scanTask)); - }); - }); - } - - /* - * Recurse through a child project's parents until we reach the root, - * and return true iff we find our target project along the way. - */ - private static boolean isAncestorOf(Project target, Project child) { - Project p = child; - while (p != null) { - if (p == target) { - return true; - } - p = p.getParent(); - } - return false; - } - - private static FileCollection compilationClasspath(ConfigurationContainer configurations) { - return configurations.getByName("compile") - .plus(configurations.getByName("compileOnly")); - } -} diff --git a/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/GenerateApi.java b/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/GenerateApi.java deleted file mode 100644 index 9c87224075..0000000000 --- a/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/GenerateApi.java +++ /dev/null @@ -1,61 +0,0 @@ -package net.corda.plugins; - -import org.gradle.api.DefaultTask; -import org.gradle.api.file.FileCollection; -import org.gradle.api.tasks.InputFiles; -import org.gradle.api.tasks.OutputFile; -import org.gradle.api.tasks.TaskAction; - -import java.io.*; -import java.nio.file.Files; - -import static java.util.Comparator.comparing; -import static java.util.stream.Collectors.toList; - -@SuppressWarnings("unused") -public class GenerateApi extends DefaultTask { - - private final File outputDir; - private String baseName; - - public GenerateApi() { - outputDir = new File(getProject().getBuildDir(), "api"); - baseName = "api-" + getProject().getName(); - } - - public void setBaseName(String baseName) { - this.baseName = baseName; - } - - @InputFiles - public FileCollection getSources() { - return getProject().files(getProject().getAllprojects().stream() - .flatMap(project -> project.getTasks() - .withType(ScanApi.class) - .matching(ScanApi::isEnabled) - .stream()) - .flatMap(scanTask -> scanTask.getTargets().getFiles().stream()) - .sorted(comparing(File::getName)) - .collect(toList()) - ); - } - - @OutputFile - public File getTarget() { - return new File(outputDir, String.format("%s-%s.txt", baseName, getProject().getVersion())); - } - - @TaskAction - public void generate() { - FileCollection apiFiles = getSources(); - if (!apiFiles.isEmpty()) { - try (OutputStream output = new BufferedOutputStream(new FileOutputStream(getTarget()))) { - for (File apiFile : apiFiles) { - Files.copy(apiFile.toPath(), output); - } - } catch (IOException e) { - getLogger().error("Failed to generate API file", e); - } - } - } -} diff --git a/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScanApi.java b/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScanApi.java deleted file mode 100644 index eb2b7e5599..0000000000 --- a/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScanApi.java +++ /dev/null @@ -1,389 +0,0 @@ -package net.corda.plugins; - -import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner; -import io.github.lukehutch.fastclasspathscanner.scanner.ClassInfo; -import io.github.lukehutch.fastclasspathscanner.scanner.FieldInfo; -import io.github.lukehutch.fastclasspathscanner.scanner.MethodInfo; -import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult; -import org.gradle.api.DefaultTask; -import org.gradle.api.file.ConfigurableFileCollection; -import org.gradle.api.file.FileCollection; -import org.gradle.api.tasks.CompileClasspath; -import org.gradle.api.tasks.Input; -import org.gradle.api.tasks.InputFiles; -import org.gradle.api.tasks.OutputFiles; -import org.gradle.api.tasks.TaskAction; - -import java.io.*; -import java.lang.annotation.Annotation; -import java.lang.annotation.Inherited; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLClassLoader; -import java.util.*; -import java.util.stream.StreamSupport; - -import static java.util.Collections.*; -import static java.util.stream.Collectors.*; - -@SuppressWarnings("unused") -public class ScanApi extends DefaultTask { - private static final int CLASS_MASK = Modifier.classModifiers(); - private static final int INTERFACE_MASK = Modifier.interfaceModifiers() & ~Modifier.ABSTRACT; - private static final int METHOD_MASK = Modifier.methodModifiers(); - private static final int FIELD_MASK = Modifier.fieldModifiers(); - private static final int VISIBILITY_MASK = Modifier.PUBLIC | Modifier.PROTECTED; - - private static final Set ANNOTATION_BLACKLIST; - static { - Set blacklist = new LinkedHashSet<>(); - blacklist.add("kotlin.jvm.JvmOverloads"); - ANNOTATION_BLACKLIST = unmodifiableSet(blacklist); - } - - /** - * This information has been lifted from: - * @link Metadata.kt - */ - private static final String KOTLIN_METADATA = "kotlin.Metadata"; - private static final String KOTLIN_CLASSTYPE_METHOD = "k"; - private static final int KOTLIN_SYNTHETIC = 3; - - private final ConfigurableFileCollection sources; - private final ConfigurableFileCollection classpath; - private final Set excludeClasses; - private final File outputDir; - private boolean verbose; - - public ScanApi() { - sources = getProject().files(); - classpath = getProject().files(); - excludeClasses = new LinkedHashSet<>(); - outputDir = new File(getProject().getBuildDir(), "api"); - } - - @InputFiles - public FileCollection getSources() { - return sources; - } - - void setSources(FileCollection sources) { - this.sources.setFrom(sources); - } - - @CompileClasspath - @InputFiles - public FileCollection getClasspath() { - return classpath; - } - - void setClasspath(FileCollection classpath) { - this.classpath.setFrom(classpath); - } - - @Input - public Collection getExcludeClasses() { - return unmodifiableSet(excludeClasses); - } - - void setExcludeClasses(Collection excludeClasses) { - this.excludeClasses.clear(); - this.excludeClasses.addAll(excludeClasses); - } - - @OutputFiles - public FileCollection getTargets() { - return getProject().files( - StreamSupport.stream(sources.spliterator(), false) - .map(this::toTarget) - .collect(toList()) - ); - } - - public boolean isVerbose() { - return verbose; - } - - void setVerbose(boolean verbose) { - this.verbose = verbose; - } - - private File toTarget(File source) { - return new File(outputDir, source.getName().replaceAll(".jar$", ".txt")); - } - - @TaskAction - public void scan() { - try (Scanner scanner = new Scanner(classpath)) { - for (File source : sources) { - scanner.scan(source); - } - } catch (IOException e) { - getLogger().error("Failed to write API file", e); - } - } - - class Scanner implements Closeable { - private final URLClassLoader classpathLoader; - private final Class metadataClass; - private final Method classTypeMethod; - - @SuppressWarnings("unchecked") - Scanner(URLClassLoader classpathLoader) { - this.classpathLoader = classpathLoader; - - Class kClass; - Method kMethod; - try { - kClass = (Class) Class.forName(KOTLIN_METADATA, true, classpathLoader); - kMethod = kClass.getDeclaredMethod(KOTLIN_CLASSTYPE_METHOD); - } catch (ClassNotFoundException | NoSuchMethodException e) { - kClass = null; - kMethod = null; - } - - metadataClass = kClass; - classTypeMethod = kMethod; - } - - Scanner(FileCollection classpath) throws MalformedURLException { - this(new URLClassLoader(toURLs(classpath))); - } - - @Override - public void close() throws IOException { - classpathLoader.close(); - } - - void scan(File source) { - File target = toTarget(source); - try ( - URLClassLoader appLoader = new URLClassLoader(new URL[]{ toURL(source) }, classpathLoader); - PrintWriter writer = new PrintWriter(target, "UTF-8") - ) { - scan(writer, appLoader); - } catch (IOException e) { - getLogger().error("API scan has failed", e); - } - } - - void scan(PrintWriter writer, ClassLoader appLoader) { - ScanResult result = new FastClasspathScanner(getScanSpecification()) - .overrideClassLoaders(appLoader) - .ignoreParentClassLoaders() - .ignoreMethodVisibility() - .ignoreFieldVisibility() - .enableMethodInfo() - .enableFieldInfo() - .verbose(verbose) - .scan(); - writeApis(writer, result); - } - - private String[] getScanSpecification() { - String[] spec = new String[2 + excludeClasses.size()]; - spec[0] = "!"; // Don't blacklist system classes from the output. - spec[1] = "-dir:"; // Ignore classes on the filesystem. - - int i = 2; - for (String excludeClass : excludeClasses) { - spec[i++] = '-' + excludeClass; - } - return spec; - } - - private void writeApis(PrintWriter writer, ScanResult result) { - Map allInfo = result.getClassNameToClassInfo(); - result.getNamesOfAllClasses().forEach(className -> { - if (className.contains(".internal.")) { - // These classes belong to internal Corda packages. - return; - } - ClassInfo classInfo = allInfo.get(className); - if (classInfo.getClassLoaders() == null) { - // Ignore classes that belong to one of our target ClassLoader's parents. - return; - } - - Class javaClass = result.classNameToClassRef(className); - if (!isVisible(javaClass.getModifiers())) { - // Excludes private and package-protected classes - return; - } - - int kotlinClassType = getKotlinClassType(javaClass); - if (kotlinClassType == KOTLIN_SYNTHETIC) { - // Exclude classes synthesised by the Kotlin compiler. - return; - } - - writeClass(writer, classInfo, javaClass.getModifiers()); - writeMethods(writer, classInfo.getMethodAndConstructorInfo()); - writeFields(writer, classInfo.getFieldInfo()); - writer.println("##"); - }); - } - - private void writeClass(PrintWriter writer, ClassInfo classInfo, int modifiers) { - if (classInfo.isAnnotation()) { - /* - * Annotation declaration. - */ - writer.append(Modifier.toString(modifiers & INTERFACE_MASK)); - writer.append(" @interface ").print(classInfo); - } else if (classInfo.isStandardClass()) { - /* - * Class declaration. - */ - List annotationNames = toNames(readClassAnnotationsFor(classInfo)); - if (!annotationNames.isEmpty()) { - writer.append(asAnnotations(annotationNames)); - } - writer.append(Modifier.toString(modifiers & CLASS_MASK)); - writer.append(" class ").print(classInfo); - Set superclasses = classInfo.getDirectSuperclasses(); - if (!superclasses.isEmpty()) { - writer.append(" extends ").print(stringOf(superclasses)); - } - Set interfaces = classInfo.getDirectlyImplementedInterfaces(); - if (!interfaces.isEmpty()) { - writer.append(" implements ").print(stringOf(interfaces)); - } - } else { - /* - * Interface declaration. - */ - List annotationNames = toNames(readInterfaceAnnotationsFor(classInfo)); - if (!annotationNames.isEmpty()) { - writer.append(asAnnotations(annotationNames)); - } - writer.append(Modifier.toString(modifiers & INTERFACE_MASK)); - writer.append(" interface ").print(classInfo); - Set superinterfaces = classInfo.getDirectSuperinterfaces(); - if (!superinterfaces.isEmpty()) { - writer.append(" extends ").print(stringOf(superinterfaces)); - } - } - writer.println(); - } - - private void writeMethods(PrintWriter writer, List methods) { - sort(methods); - for (MethodInfo method : methods) { - if (isVisible(method.getAccessFlags()) // Only public and protected methods - && isValid(method.getAccessFlags(), METHOD_MASK) // Excludes bridge and synthetic methods - && !isKotlinInternalScope(method)) { - writer.append(" ").println(filterAnnotationsFor(method)); - } - } - } - - private void writeFields(PrintWriter output, List fields) { - sort(fields); - for (FieldInfo field : fields) { - if (isVisible(field.getAccessFlags()) && isValid(field.getAccessFlags(), FIELD_MASK)) { - output.append(" ").println(field); - } - } - } - - private int getKotlinClassType(Class javaClass) { - if (metadataClass != null) { - Annotation metadata = javaClass.getAnnotation(metadataClass); - if (metadata != null) { - try { - return (int) classTypeMethod.invoke(metadata); - } catch (IllegalAccessException | InvocationTargetException e) { - getLogger().error("Failed to read Kotlin annotation", e); - } - } - } - return 0; - } - - private List toNames(Collection classes) { - return classes.stream() - .map(ClassInfo::toString) - .filter(ScanApi::isApplicationClass) - .collect(toList()); - } - - private Set readClassAnnotationsFor(ClassInfo classInfo) { - Set annotations = new HashSet<>(classInfo.getAnnotations()); - annotations.addAll(selectInheritedAnnotations(classInfo.getSuperclasses())); - annotations.addAll(selectInheritedAnnotations(classInfo.getImplementedInterfaces())); - return annotations; - } - - private Set readInterfaceAnnotationsFor(ClassInfo classInfo) { - Set annotations = new HashSet<>(classInfo.getAnnotations()); - annotations.addAll(selectInheritedAnnotations(classInfo.getSuperinterfaces())); - return annotations; - } - - /** - * Returns those annotations which have themselves been annotated as "Inherited". - */ - private List selectInheritedAnnotations(Collection classes) { - return classes.stream() - .flatMap(cls -> cls.getAnnotations().stream()) - .filter(ann -> ann.hasMetaAnnotation(Inherited.class.getName())) - .collect(toList()); - } - - private MethodInfo filterAnnotationsFor(MethodInfo method) { - return new MethodInfo( - method.getClassName(), - method.getMethodName(), - method.getAccessFlags(), - method.getTypeDescriptor(), - method.getAnnotationNames().stream() - .filter(ScanApi::isVisibleAnnotation) - .collect(toList()) - ); - } - } - - private static boolean isVisibleAnnotation(String annotationName) { - return !ANNOTATION_BLACKLIST.contains(annotationName); - } - - private static boolean isKotlinInternalScope(MethodInfo method) { - return method.getMethodName().indexOf('$') >= 0; - } - - private static boolean isValid(int modifiers, int mask) { - return (modifiers & mask) == modifiers; - } - - private static boolean isVisible(int accessFlags) { - return (accessFlags & VISIBILITY_MASK) != 0; - } - - private static String stringOf(Collection items) { - return items.stream().map(ClassInfo::toString).collect(joining(", ")); - } - - private static String asAnnotations(Collection items) { - return items.stream().collect(joining(" @", "@", " ")); - } - - private static boolean isApplicationClass(String typeName) { - return !typeName.startsWith("java.") && !typeName.startsWith("kotlin."); - } - - private static URL toURL(File file) throws MalformedURLException { - return file.toURI().toURL(); - } - - private static URL[] toURLs(Iterable files) throws MalformedURLException { - List urls = new LinkedList<>(); - for (File file : files) { - urls.add(toURL(file)); - } - return urls.toArray(new URL[urls.size()]); - } -} diff --git a/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScannerExtension.java b/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScannerExtension.java deleted file mode 100644 index 4e77437cfc..0000000000 --- a/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScannerExtension.java +++ /dev/null @@ -1,37 +0,0 @@ -package net.corda.plugins; - -import java.util.List; - -import static java.util.Collections.emptyList; - -@SuppressWarnings("unused") -public class ScannerExtension { - - private boolean verbose; - private boolean enabled = true; - private List excludeClasses = emptyList(); - - public boolean isVerbose() { - return verbose; - } - - public void setVerbose(boolean verbose) { - this.verbose = verbose; - } - - public boolean isEnabled() { - return enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public List getExcludeClasses() { - return excludeClasses; - } - - public void setExcludeClasses(List excludeClasses) { - this.excludeClasses = excludeClasses; - } -} diff --git a/gradle-plugins/api-scanner/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.api-scanner.properties b/gradle-plugins/api-scanner/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.api-scanner.properties deleted file mode 100644 index fc9e2277a5..0000000000 --- a/gradle-plugins/api-scanner/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.api-scanner.properties +++ /dev/null @@ -1 +0,0 @@ -implementation-class=net.corda.plugins.ApiScanner diff --git a/gradle-plugins/build.gradle b/gradle-plugins/build.gradle deleted file mode 100644 index 32763f85f6..0000000000 --- a/gradle-plugins/build.gradle +++ /dev/null @@ -1,84 +0,0 @@ -// This script exists just to allow bootstrapping the gradle plugins if maven central or jcenter are unavailable -// or if you are developing these plugins. See the readme for more information. - -buildscript { - // For sharing constants between builds - Properties constants = new Properties() - file("$projectDir/../constants.properties").withInputStream { constants.load(it) } - - // If you bump this version you must re-bootstrap the codebase. See the README for more information. - ext { - gradle_plugins_version = constants.getProperty("gradlePluginsVersion") - bouncycastle_version = constants.getProperty("bouncycastleVersion") - typesafe_config_version = constants.getProperty("typesafeConfigVersion") - jsr305_version = constants.getProperty("jsr305Version") - kotlin_version = constants.getProperty("kotlinVersion") - artifactory_plugin_version = constants.getProperty('artifactoryPluginVersion') - snake_yaml_version = constants.getProperty('snakeYamlVersion') - } - - repositories { - mavenLocal() - jcenter() - } - - dependencies { - classpath "net.corda.plugins:publish-utils:$gradle_plugins_version" - classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath "org.jfrog.buildinfo:build-info-extractor-gradle:$artifactory_plugin_version" - } -} - -apply plugin: 'net.corda.plugins.publish-utils' -apply plugin: 'com.jfrog.artifactory' - -allprojects { - version gradle_plugins_version - group 'net.corda.plugins' -} - -bintrayConfig { - user = System.getenv('CORDA_BINTRAY_USER') - key = System.getenv('CORDA_BINTRAY_KEY') - repo = 'corda' - org = 'r3' - licenses = ['Apache-2.0'] - vcsUrl = 'https://github.com/corda/corda' - projectUrl = 'https://github.com/corda/corda' - gpgSign = true - gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE') - publications = ['cordformation', 'quasar-utils', 'cordform-common', 'api-scanner', 'cordapp'] - license { - name = 'Apache-2.0' - url = 'https://www.apache.org/licenses/LICENSE-2.0' - distribution = 'repo' - } - developer { - id = 'R3' - name = 'R3' - email = 'dev@corda.net' - } -} - -artifactory { - publish { - contextUrl = 'https://ci-artifactory.corda.r3cev.com/artifactory' - repository { - repoKey = 'corda-dev' - username = 'teamcity' - password = System.getenv('CORDA_ARTIFACTORY_PASSWORD') - } - - defaults { - // Publish utils does not have a publish block because it would be circular for it to apply it's own - // extensions to itself - if(project.name == 'publish-utils') { - publications('publishUtils') - // Root project applies the plugin (for this block) but does not need to be published - } else if(project != rootProject) { - publications(project.extensions.publish.name()) - } - } - } -} \ No newline at end of file diff --git a/gradle-plugins/cordapp/README.md b/gradle-plugins/cordapp/README.md deleted file mode 100644 index 6b0cebe690..0000000000 --- a/gradle-plugins/cordapp/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# Cordapp Gradle Plugin - -## Purpose - -To transform any project this plugin is applied to into a cordapp project that generates a cordapp JAR. - -## Effects - -Will modify the default JAR task to create a CorDapp format JAR instead [see here](https://docs.corda.net/cordapp-build-systems.html) -for more information. \ No newline at end of file diff --git a/gradle-plugins/cordapp/build.gradle b/gradle-plugins/cordapp/build.gradle deleted file mode 100644 index 3d1ecb6b53..0000000000 --- a/gradle-plugins/cordapp/build.gradle +++ /dev/null @@ -1,19 +0,0 @@ -apply plugin: 'kotlin' -apply plugin: 'net.corda.plugins.publish-utils' -apply plugin: 'com.jfrog.artifactory' - -description 'Turns a project into a cordapp project that produces cordapp fat JARs' - -repositories { - mavenCentral() - jcenter() -} - -dependencies { - compile gradleApi() - compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" -} - -publish { - name project.name -} \ No newline at end of file diff --git a/gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/CordappPlugin.kt b/gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/CordappPlugin.kt deleted file mode 100644 index 624194fd32..0000000000 --- a/gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/CordappPlugin.kt +++ /dev/null @@ -1,75 +0,0 @@ -package net.corda.plugins - -import org.gradle.api.* -import org.gradle.api.artifacts.* -import org.gradle.jvm.tasks.Jar -import java.io.File - -/** - * The Cordapp plugin will turn a project into a cordapp project which builds cordapp JARs with the correct format - * and with the information needed to run on Corda. - */ -class CordappPlugin : Plugin { - override fun apply(project: Project) { - project.logger.info("Configuring ${project.name} as a cordapp") - - Utils.createCompileConfiguration("cordapp", project) - Utils.createCompileConfiguration("cordaCompile", project) - - val configuration: Configuration = project.configurations.create("cordaRuntime") - configuration.isTransitive = false - project.configurations.single { it.name == "runtime" }.extendsFrom(configuration) - - configureCordappJar(project) - } - - /** - * Configures this project's JAR as a Cordapp JAR - */ - private fun configureCordappJar(project: Project) { - // Note: project.afterEvaluate did not have full dependency resolution completed, hence a task is used instead - val task = project.task("configureCordappFatJar") - val jarTask = project.tasks.getByName("jar") as Jar - task.doLast { - jarTask.from(getDirectNonCordaDependencies(project).map { project.zipTree(it)}).apply { - exclude("META-INF/*.SF") - exclude("META-INF/*.DSA") - exclude("META-INF/*.RSA") - } - } - jarTask.dependsOn(task) - } - - private fun getDirectNonCordaDependencies(project: Project): Set { - project.logger.info("Finding direct non-corda dependencies for inclusion in CorDapp JAR") - val excludes = listOf( - mapOf("group" to "org.jetbrains.kotlin", "name" to "kotlin-stdlib"), - mapOf("group" to "org.jetbrains.kotlin", "name" to "kotlin-stdlib-jre8"), - mapOf("group" to "org.jetbrains.kotlin", "name" to "kotlin-reflect"), - mapOf("group" to "co.paralleluniverse", "name" to "quasar-core") - ) - - val runtimeConfiguration = project.configuration("runtime") - // The direct dependencies of this project - val excludeDeps = project.configuration("cordapp").allDependencies + - project.configuration("cordaCompile").allDependencies + - project.configuration("cordaRuntime").allDependencies - val directDeps = runtimeConfiguration.allDependencies - excludeDeps - // We want to filter out anything Corda related or provided by Corda, like kotlin-stdlib and quasar - val filteredDeps = directDeps.filter { dep -> - excludes.none { exclude -> (exclude["group"] == dep.group) && (exclude["name"] == dep.name) } - } - filteredDeps.forEach { - // net.corda or com.r3.corda.enterprise may be a core dependency which shouldn't be included in this cordapp so give a warning - val group = it.group?.toString() ?: "" - if (group.startsWith("net.corda.") || group.startsWith("com.r3.corda.enterprise.")) { - project.logger.warn("You appear to have included a Corda platform component ($it) using a 'compile' or 'runtime' dependency." + - "This can cause node stability problems. Please use 'corda' instead." + - "See http://docs.corda.net/cordapp-build-systems.html") - } else { - project.logger.info("Including dependency in CorDapp JAR: $it") - } - } - return filteredDeps.map { runtimeConfiguration.files(it) }.flatten().toSet() - } -} diff --git a/gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/Utils.kt b/gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/Utils.kt deleted file mode 100644 index 7572cd9876..0000000000 --- a/gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/Utils.kt +++ /dev/null @@ -1,35 +0,0 @@ -package net.corda.plugins - -import org.gradle.api.Project -import org.gradle.api.Task -import org.gradle.api.artifacts.Configuration -import org.gradle.api.plugins.ExtraPropertiesExtension - -/** - * Mimics the "project.ext" functionality in groovy which provides a direct - * accessor to the "ext" extention (See: ExtraPropertiesExtension) - */ -@Suppress("UNCHECKED_CAST") -fun Project.ext(name: String): T = (extensions.findByName("ext") as ExtraPropertiesExtension).get(name) as T -fun Project.configuration(name: String): Configuration = configurations.single { it.name == name } - -class Utils { - companion object { - @JvmStatic - fun createCompileConfiguration(name: String, project: Project) { - if(!project.configurations.any { it.name == name }) { - val configuration = project.configurations.create(name) - configuration.isTransitive = false - project.configurations.single { it.name == "compile" }.extendsFrom(configuration) - } - } - fun createRuntimeConfiguration(name: String, project: Project) { - if(!project.configurations.any { it.name == name }) { - val configuration = project.configurations.create(name) - configuration.isTransitive = false - project.configurations.single { it.name == "runtime" }.extendsFrom(configuration) - } - } - } - -} \ No newline at end of file diff --git a/gradle-plugins/cordapp/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.cordapp.properties b/gradle-plugins/cordapp/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.cordapp.properties deleted file mode 100644 index 90871e27c8..0000000000 --- a/gradle-plugins/cordapp/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.cordapp.properties +++ /dev/null @@ -1 +0,0 @@ -implementation-class=net.corda.plugins.CordappPlugin diff --git a/gradle-plugins/cordform-common/README.md b/gradle-plugins/cordform-common/README.md deleted file mode 100644 index 8b83c20e93..0000000000 --- a/gradle-plugins/cordform-common/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Cordform Common - -This project contains common node types that both the Corda gradle plugin suite and Corda project -require in order to build Corda nodes. \ No newline at end of file diff --git a/gradle-plugins/cordform-common/build.gradle b/gradle-plugins/cordform-common/build.gradle deleted file mode 100644 index be2fa0cf16..0000000000 --- a/gradle-plugins/cordform-common/build.gradle +++ /dev/null @@ -1,24 +0,0 @@ -apply plugin: 'java' -apply plugin: 'maven-publish' -apply plugin: 'net.corda.plugins.publish-utils' -apply plugin: 'com.jfrog.artifactory' - -repositories { - mavenCentral() -} - -// This tracks the gradle plugins version and not Corda -version gradle_plugins_version -group 'net.corda.plugins' - -dependencies { - // JSR 305: Nullability annotations - compile "com.google.code.findbugs:jsr305:$jsr305_version" - - // TypeSafe Config: for simple and human friendly config files. - compile "com.typesafe:config:$typesafe_config_version" -} - -publish { - name project.name -} \ No newline at end of file diff --git a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformContext.java b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformContext.java deleted file mode 100644 index 7687f68a11..0000000000 --- a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformContext.java +++ /dev/null @@ -1,7 +0,0 @@ -package net.corda.cordform; - -import java.nio.file.Path; - -public interface CordformContext { - Path baseDirectory(String nodeName); -} diff --git a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformDefinition.java b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformDefinition.java deleted file mode 100644 index fc62b1bbee..0000000000 --- a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformDefinition.java +++ /dev/null @@ -1,40 +0,0 @@ -package net.corda.cordform; - -import javax.annotation.Nonnull; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.List; -import java.util.function.Consumer; - -public abstract class CordformDefinition { - private Path nodesDirectory = Paths.get("build", "nodes"); - private final List> nodeConfigurers = new ArrayList<>(); - private final List cordappPackages = new ArrayList<>(); - - public Path getNodesDirectory() { - return nodesDirectory; - } - - public void setNodesDirectory(Path nodesDirectory) { - this.nodesDirectory = nodesDirectory; - } - - public List> getNodeConfigurers() { - return nodeConfigurers; - } - - public void addNode(Consumer configurer) { - nodeConfigurers.add(configurer); - } - - public List getCordappPackages() { - return cordappPackages; - } - - /** - * Make arbitrary changes to the node directories before they are started. - * @param context Lookup of node directory by node name. - */ - public abstract void setup(@Nonnull CordformContext context); -} diff --git a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformNode.java b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformNode.java deleted file mode 100644 index c3dc01a5da..0000000000 --- a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformNode.java +++ /dev/null @@ -1,190 +0,0 @@ -package net.corda.cordform; - -import com.typesafe.config.Config; -import com.typesafe.config.ConfigFactory; -import com.typesafe.config.ConfigValueFactory; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.List; -import java.util.Map; - -import static java.util.Collections.emptyList; - -public class CordformNode implements NodeDefinition { - /** - * Path relative to the running node where the serialized NodeInfos are stored. - */ - public static final String NODE_INFO_DIRECTORY = "additional-node-infos"; - - protected static final String DEFAULT_HOST = "localhost"; - - /** - * Name of the node. Node will be placed in directory based on this name - all lowercase with whitespaces removed. - * Actual node name inside node.conf will be as set here. - */ - private String name; - - public String getName() { - return name; - } - - /** - * p2p Port. - */ - private int p2pPort = 10002; - - public int getP2pPort() { return p2pPort; } - - /** - * RPC Port. - */ - private int rpcPort = 10003; - - public int getRpcPort() { return rpcPort; } - - /** - * Set the RPC users for this node. This configuration block allows arbitrary configuration. - * The recommended current structure is: - * [[['username': "username_here", 'password': "password_here", 'permissions': ["permissions_here"]]] - * The above is a list to a map of keys to values using Groovy map and list shorthands. - * - * Incorrect configurations will not cause a DSL error. - */ - public List> rpcUsers = emptyList(); - - /** - * Apply the notary configuration if this node is a notary. The map is the config structure of - * net.corda.node.services.config.NotaryConfig - */ - public Map notary = null; - - public Map extraConfig = null; - - protected Config config = ConfigFactory.empty(); - - public Config getConfig() { - return config; - } - - /** - * Set the name of the node. - * - * @param name The node name. - */ - public void name(String name) { - this.name = name; - setValue("myLegalName", name); - } - - /** - * Get the artemis address for this node. - * - * @return This node's P2P address. - */ - @Nonnull - public String getP2pAddress() { - return config.getString("p2pAddress"); - } - - /** - * Set the Artemis P2P port for this node on localhost. - * - * @param p2pPort The Artemis messaging queue port. - */ - public void p2pPort(int p2pPort) { - p2pAddress(DEFAULT_HOST + ':' + p2pPort); - this.p2pPort = p2pPort; - } - - /** - * Set the Artemis P2P address for this node. - * - * @param p2pAddress The Artemis messaging queue host and port. - */ - public void p2pAddress(String p2pAddress) { - setValue("p2pAddress", p2pAddress); - } - - /** - * Returns the RPC address for this node, or null if one hasn't been specified. - */ - @Nullable - public String getRpcAddress() { - if (config.hasPath("rpcSettings.address")) { - return config.getConfig("rpcSettings").getString("address"); - } - return getOptionalString("rpcAddress"); - } - - /** - * Set the Artemis RPC port for this node on localhost. - * - * @param rpcPort The Artemis RPC queue port. - * @deprecated Use {@link CordformNode#rpcSettings(RpcSettings)} instead. - */ - @Deprecated - public void rpcPort(int rpcPort) { - rpcAddress(DEFAULT_HOST + ':' + rpcPort); - this.rpcPort = rpcPort; - } - - /** - * Set the Artemis RPC address for this node. - * - * @param rpcAddress The Artemis RPC queue host and port. - * @deprecated Use {@link CordformNode#rpcSettings(RpcSettings)} instead. - */ - @Deprecated - public void rpcAddress(String rpcAddress) { - setValue("rpcAddress", rpcAddress); - } - - /** - * Returns the address of the web server that will connect to the node, or null if one hasn't been specified. - */ - @Nullable - public String getWebAddress() { - return getOptionalString("webAddress"); - } - - /** - * Configure a webserver to connect to the node via RPC. This port will specify the port it will listen on. The node - * must have an RPC address configured. - */ - public void webPort(int webPort) { - webAddress(DEFAULT_HOST + ':' + webPort); - } - - /** - * Configure a webserver to connect to the node via RPC. This address will specify the port it will listen on. The node - * must have an RPC address configured. - */ - public void webAddress(String webAddress) { - setValue("webAddress", webAddress); - } - - /** - * Specifies RPC settings for the node. - */ - public void rpcSettings(RpcSettings settings) { - config = settings.addTo("rpcSettings", config); - } - - /** - * Set the path to a file with optional properties, which are appended to the generated node.conf file. - * - * @param configFile The file path. - */ - public void configFile(String configFile) { - setValue("configFile", configFile); - } - - private String getOptionalString(String path) { - return config.hasPath(path) ? config.getString(path) : null; - } - - private void setValue(String path, Object value) { - config = config.withValue(path, ConfigValueFactory.fromAnyRef(value)); - } -} diff --git a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/NodeDefinition.java b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/NodeDefinition.java deleted file mode 100644 index 0b86b98627..0000000000 --- a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/NodeDefinition.java +++ /dev/null @@ -1,9 +0,0 @@ -package net.corda.cordform; - -import com.typesafe.config.Config; - -public interface NodeDefinition { - String getName(); - - Config getConfig(); -} diff --git a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/RpcSettings.java b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/RpcSettings.java deleted file mode 100644 index 1869733271..0000000000 --- a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/RpcSettings.java +++ /dev/null @@ -1,80 +0,0 @@ -package net.corda.cordform; - -import com.typesafe.config.Config; -import com.typesafe.config.ConfigFactory; -import com.typesafe.config.ConfigValueFactory; - -public final class RpcSettings { - - private Config config = ConfigFactory.empty(); - - private int port = 10003; - private int adminPort = 10005; - - public int getPort() { - return port; - } - - public int getAdminPort() { - return adminPort; - } - - /** - * RPC address for the node. - */ - public final void address(final String value) { - setValue("address", value); - } - - /** - * RPC Port for the node - */ - public final void port(final int value) { - this.port = value; - setValue("address", "localhost:"+port); - } - - /** - * RPC admin address for the node (necessary if [useSsl] is false or unset). - */ - public final void adminAddress(final String value) { - setValue("adminAddress", value); - } - - public final void adminPort(final int value) { - this.adminPort = value; - setValue("adminAddress", "localhost:"+adminPort); - } - - /** - * Specifies whether the node RPC layer will require SSL from clients. - */ - public final void useSsl(final Boolean value) { - setValue("useSsl", value); - } - - /** - * Specifies whether the RPC broker is separate from the node. - */ - public final void standAloneBroker(final Boolean value) { - setValue("standAloneBroker", value); - } - - /** - * Specifies SSL certificates options for the RPC layer. - */ - public final void ssl(final SslOptions options) { - config = options.addTo("ssl", config); - } - - public final Config addTo(final String key, final Config config) { - if (this.config.isEmpty()) { - return config; - } - return config.withValue(key, this.config.root()); - } - - private void setValue(String path, Object value) { - config = config.withValue(path, ConfigValueFactory.fromAnyRef(value)); - } -} diff --git a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/SslOptions.java b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/SslOptions.java deleted file mode 100644 index 1444d4ed8c..0000000000 --- a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/SslOptions.java +++ /dev/null @@ -1,56 +0,0 @@ -package net.corda.cordform; - -import com.typesafe.config.Config; -import com.typesafe.config.ConfigFactory; -import com.typesafe.config.ConfigValueFactory; - -public final class SslOptions { - - private Config config = ConfigFactory.empty(); - - /** - * Password for the keystore. - */ - public final void keyStorePassword(final String value) { - setValue("keyStorePassword", value); - } - - /** - * Password for the truststore. - */ - public final void trustStorePassword(final String value) { - setValue("trustStorePassword", value); - } - - /** - * Directory under which key stores are to be placed. - */ - public final void certificatesDirectory(final String value) { - setValue("certificatesDirectory", value); - } - - /** - * Absolute path to SSL keystore. Default: "[certificatesDirectory]/sslkeystore.jks" - */ - public final void sslKeystore(final String value) { - setValue("sslKeystore", value); - } - - /** - * Absolute path to SSL truststore. Default: "[certificatesDirectory]/truststore.jks" - */ - public final void trustStoreFile(final String value) { - setValue("trustStoreFile", value); - } - - public final Config addTo(final String key, final Config config) { - if (this.config.isEmpty()) { - return config; - } - return config.withValue(key, this.config.root()); - } - - private void setValue(String path, Object value) { - config = config.withValue(path, ConfigValueFactory.fromAnyRef(value)); - } -} diff --git a/gradle-plugins/cordformation/README.rst b/gradle-plugins/cordformation/README.rst deleted file mode 100644 index f619737a91..0000000000 --- a/gradle-plugins/cordformation/README.rst +++ /dev/null @@ -1 +0,0 @@ -Please refer to the documentation in /doc/build/html/running-a-node.html#cordformation. \ No newline at end of file diff --git a/gradle-plugins/cordformation/build.gradle b/gradle-plugins/cordformation/build.gradle deleted file mode 100644 index 5ac1e33473..0000000000 --- a/gradle-plugins/cordformation/build.gradle +++ /dev/null @@ -1,64 +0,0 @@ -buildscript { - repositories { - mavenCentral() - } - - dependencies { - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - -apply plugin: 'kotlin' -apply plugin: 'net.corda.plugins.publish-utils' -apply plugin: 'com.jfrog.artifactory' - -description 'A small gradle plugin for adding some basic Quasar tasks and configurations to reduce build.gradle bloat.' - -repositories { - mavenCentral() -} - -configurations { - noderunner - compile.extendsFrom noderunner -} - -sourceSets { - runnodes { - kotlin { - srcDir file('src/noderunner/kotlin') - compileClasspath += configurations.noderunner - } - } -} - -dependencies { - compile gradleApi() - compile project(":cordapp") - compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" - - noderunner "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" - - compile project(':cordform-common') - // Docker-compose file generation - compile "org.yaml:snakeyaml:$snake_yaml_version" -} - -task createNodeRunner(type: Jar, dependsOn: [classes]) { - manifest { - attributes('Main-Class': 'net.corda.plugins.NodeRunnerKt') - } - classifier = 'fatjar' - from { configurations.noderunner.collect { it.isDirectory() ? it : zipTree(it) } } - from sourceSets.runnodes.output -} - -jar { - from(createNodeRunner) { - rename { 'net/corda/plugins/runnodes.jar' } - } -} - -publish { - name project.name -} diff --git a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Baseform.kt b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Baseform.kt deleted file mode 100644 index ea528aaf08..0000000000 --- a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Baseform.kt +++ /dev/null @@ -1,174 +0,0 @@ -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 -import java.nio.file.Path -import java.nio.file.Paths -import java.util.jar.JarInputStream - -/** - * Creates nodes based on the configuration of this task in the gradle configuration DSL. - * - * See documentation for examples. - */ -@Suppress("unused") -open class Baseform : DefaultTask() { - private companion object { - val nodeJarName = "corda.jar" - private val defaultDirectory: Path = Paths.get("build", "nodes") - } - - /** - * Optionally the name of a CordformDefinition subclass to which all configuration will be delegated. - */ - @Suppress("MemberVisibilityCanPrivate") - var definitionClass: String? = null - var directory = defaultDirectory - protected val nodes = mutableListOf() - - /** - * Set the directory to install nodes into. - * - * @param directory The directory the nodes will be installed into. - */ - fun directory(directory: String) { - this.directory = Paths.get(directory) - } - - /** - * Add a node configuration. - * - * @param configureClosure A node configuration that will be deployed. - */ - @Suppress("MemberVisibilityCanPrivate") - fun node(configureClosure: Closure) { - nodes += project.configure(Node(project), configureClosure) as Node - } - - /** - * Add a node configuration - * - * @param configureFunc A node configuration that will be deployed - */ - @Suppress("MemberVisibilityCanPrivate") - fun node(configureFunc: Node.() -> Any?): Node { - val node = Node(project).apply { configureFunc() } - nodes += node - return node - } - - /** - * Returns a node by name. - * - * @param name The name of the node as specified in the node configuration DSL. - * @return A node instance. - */ - private fun getNodeByName(name: String): Node? = nodes.firstOrNull { it.name == name } - - /** - * The definitionClass needn't be compiled until just before our build method, so we load it manually via sourceSets.main.runtimeClasspath. - */ - private fun loadCordformDefinition(): CordformDefinition { - val plugin = project.convention.getPlugin(JavaPluginConvention::class.java) - val classpath = plugin.sourceSets.getByName(MAIN_SOURCE_SET_NAME).runtimeClasspath - val urls = classpath.files.map { it.toURI().toURL() }.toTypedArray() - return URLClassLoader(urls, CordformDefinition::class.java.classLoader) - .loadClass(definitionClass) - .asSubclass(CordformDefinition::class.java) - .newInstance() - } - - /** - * The NetworkBootstrapper needn't be compiled until just before our build method, so we load it manually via sourceSets.main.runtimeClasspath. - */ - private fun loadNetworkBootstrapperClass(): Class<*> { - val plugin = project.convention.getPlugin(JavaPluginConvention::class.java) - val classpath = plugin.sourceSets.getByName(MAIN_SOURCE_SET_NAME).runtimeClasspath - val urls = classpath.files.map { it.toURI().toURL() }.toTypedArray() - return URLClassLoader(urls, javaClass.classLoader).loadClass("net.corda.nodeapi.internal.network.NetworkBootstrapper") - } - - /** - * Installs the corda fat JAR to the root directory, for the network bootstrapper to use. - */ - protected fun installCordaJar() { - val cordaJar = Cordformation.verifyAndGetRuntimeJar(project, "corda") - project.copy { - it.apply { - from(cordaJar) - into(directory) - rename(cordaJar.name, nodeJarName) - fileMode = Cordformation.executableFileMode - } - } - } - - protected fun initializeConfiguration() { - if (definitionClass != null) { - val cd = loadCordformDefinition() - // If the user has specified their own directory (even if it's the same default path) then let them know - // it's not used and should just rely on the one in CordformDefinition - require(directory === defaultDirectory) { - "'directory' cannot be used when 'definitionClass' is specified. Use CordformDefinition.nodesDirectory instead." - } - directory = cd.nodesDirectory - val cordapps = cd.getMatchingCordapps() - cd.nodeConfigurers.forEach { - val node = node { } - it.accept(node) - node.additionalCordapps.addAll(cordapps) - node.rootDir(directory) - } - cd.setup { nodeName -> project.projectDir.toPath().resolve(getNodeByName(nodeName)!!.nodeDir.toPath()) } - } else { - nodes.forEach { - it.rootDir(directory) - } - } - } - - protected fun bootstrapNetwork() { - val networkBootstrapperClass = loadNetworkBootstrapperClass() - val networkBootstrapper = networkBootstrapperClass.newInstance() - val bootstrapMethod = networkBootstrapperClass.getMethod("bootstrap", Path::class.java).apply { isAccessible = true } - // Call NetworkBootstrapper.bootstrap - try { - val rootDir = project.projectDir.toPath().resolve(directory).toAbsolutePath().normalize() - bootstrapMethod.invoke(networkBootstrapper, rootDir) - } catch (e: InvocationTargetException) { - throw e.cause!! - } - } - - private fun CordformDefinition.getMatchingCordapps(): List { - val cordappJars = project.configuration("cordapp").files - return cordappPackages.map { `package` -> - val cordappsWithPackage = cordappJars.filter { it.containsPackage(`package`) } - when (cordappsWithPackage.size) { - 0 -> throw IllegalArgumentException("There are no cordapp dependencies containing the package $`package`") - 1 -> cordappsWithPackage[0] - else -> throw IllegalArgumentException("More than one cordapp dependency contains the package $`package`: $cordappsWithPackage") - } - } - } - - private fun File.containsPackage(`package`: String): Boolean { - JarInputStream(inputStream()).use { - while (true) { - val name = it.nextJarEntry?.name ?: break - if (name.endsWith(".class") && name.replace('/', '.').startsWith(`package`)) { - return true - } - } - return false - } - } -} diff --git a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordform.kt b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordform.kt deleted file mode 100644 index f207dc1818..0000000000 --- a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordform.kt +++ /dev/null @@ -1,66 +0,0 @@ -package net.corda.plugins - -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.nio.file.Path -import java.nio.file.Paths - -/** - * Creates nodes based on the configuration of this task in the gradle configuration DSL. - * - * See documentation for examples. - */ -@Suppress("unused") -open class Cordform : Baseform() { - private companion object { - val nodeJarName = "corda.jar" - private val defaultDirectory: Path = Paths.get("build", "nodes") - } - - /** - * Installs the run script into the nodes directory. - */ - private fun installRunScript() { - project.copy { - it.apply { - from(Cordformation.getPluginFile(project, "net/corda/plugins/runnodes.jar")) - fileMode = Cordformation.executableFileMode - into("$directory/") - } - } - - project.copy { - it.apply { - from(Cordformation.getPluginFile(project, "net/corda/plugins/runnodes")) - // Replaces end of line with lf to avoid issues with the bash interpreter and Windows style line endings. - filter(mapOf("eol" to FixCrLfFilter.CrLf.newInstance("lf")), FixCrLfFilter::class.java) - fileMode = Cordformation.executableFileMode - into("$directory/") - } - } - - project.copy { - it.apply { - from(Cordformation.getPluginFile(project, "net/corda/plugins/runnodes.bat")) - into("$directory/") - } - } - } - - /** - * This task action will create and install the nodes based on the node configurations added. - */ - @TaskAction - fun build() { - project.logger.info("Running Cordform task") - initializeConfiguration() - nodes.forEach(Node::installConfig) - installCordaJar() - installRunScript() - bootstrapNetwork() - nodes.forEach(Node::build) - } -} diff --git a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordformation.kt b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordformation.kt deleted file mode 100644 index 43444ca270..0000000000 --- a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordformation.kt +++ /dev/null @@ -1,62 +0,0 @@ -package net.corda.plugins - -import org.gradle.api.Plugin -import org.gradle.api.Project -import java.io.File - -/** - * The Cordformation plugin deploys nodes to a directory in a state ready to be used by a developer for experimentation, - * testing, and debugging. It will prepopulate several fields in the configuration and create a simple node runner. - */ -class Cordformation : Plugin { - internal companion object { - const val CORDFORMATION_TYPE = "cordformationInternal" - - /** - * Gets a resource file from this plugin's JAR file. - * - * @param project The project environment this plugin executes in. - * @param filePathInJar The file in the JAR, relative to root, you wish to access. - * @return A file handle to the file in the JAR. - */ - fun getPluginFile(project: Project, filePathInJar: String): File { - val archive = project.rootProject.buildscript.configurations - .single { it.name == "classpath" } - .first { it.name.contains("cordformation") } - return project.rootProject.resources.text - .fromArchiveEntry(archive, filePathInJar) - .asFile() - } - - /** - * Gets a current built corda jar file - * - * @param project The project environment this plugin executes in. - * @param jarName The name of the JAR you wish to access. - * @return A file handle to the file in the JAR. - */ - fun verifyAndGetRuntimeJar(project: Project, jarName: String): File { - val releaseVersion = project.rootProject.ext("corda_release_version") - val maybeJar = project.configuration("runtime").filter { - "$jarName-$releaseVersion.jar" in it.toString() || "$jarName-enterprise-$releaseVersion.jar" in it.toString() - } - if (maybeJar.isEmpty) { - throw IllegalStateException("No $jarName JAR found. Have you deployed the Corda project to Maven? Looked for \"$jarName-$releaseVersion.jar\"") - } else { - val jar = maybeJar.singleFile - require(jar.isFile) - return jar - } - } - - val executableFileMode = "0755".toInt(8) - } - - override fun apply(project: Project) { - Utils.createCompileConfiguration("cordapp", project) - Utils.createRuntimeConfiguration(CORDFORMATION_TYPE, project) - // TODO: improve how we re-use existing declared external variables from root gradle.build - val jolokiaVersion = try { project.rootProject.ext("jolokia_version") } catch (e: Exception) { "1.3.7" } - project.dependencies.add(CORDFORMATION_TYPE, "org.jolokia:jolokia-jvm:$jolokiaVersion:agent") - } -} diff --git a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Dockerform.kt b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Dockerform.kt deleted file mode 100644 index 4e86e5caac..0000000000 --- a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Dockerform.kt +++ /dev/null @@ -1,66 +0,0 @@ -package net.corda.plugins - -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 org.yaml.snakeyaml.DumperOptions -import java.nio.file.Path -import java.nio.file.Paths -import org.yaml.snakeyaml.Yaml -import java.nio.charset.StandardCharsets -import java.nio.file.Files - -/** - * Creates docker-compose file and image definitions based on the configuration of this task in the gradle configuration DSL. - * - * See documentation for examples. - */ -@Suppress("unused") -open class Dockerform : Baseform() { - private companion object { - val nodeJarName = "corda.jar" - private val defaultDirectory: Path = Paths.get("build", "docker") - - private val dockerComposeFileVersion = "3" - - private val yamlOptions = DumperOptions().apply { - indent = 2 - defaultFlowStyle = DumperOptions.FlowStyle.BLOCK - } - private val yaml = Yaml(yamlOptions) - } - - private val directoryPath = project.projectDir.toPath().resolve(directory) - - val dockerComposePath = directoryPath.resolve("docker-compose.yml") - - /** - * This task action will create and install the nodes based on the node configurations added. - */ - @TaskAction - fun build() { - project.logger.info("Running Cordform task") - initializeConfiguration() - nodes.forEach(Node::installDockerConfig) - installCordaJar() - bootstrapNetwork() - nodes.forEach(Node::buildDocker) - - - // Transform nodes path the absolute ones - val services = nodes.map { it.containerName to mapOf( - "build" to directoryPath.resolve(it.nodeDir.name).toAbsolutePath().toString(), - "ports" to listOf(it.rpcPort)) }.toMap() - - - val dockerComposeObject = mapOf( - "version" to dockerComposeFileVersion, - "services" to services) - - val dockerComposeContent = yaml.dump(dockerComposeObject) - - Files.write(dockerComposePath, dockerComposeContent.toByteArray(StandardCharsets.UTF_8)) - } -} diff --git a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt deleted file mode 100644 index 10c408cc71..0000000000 --- a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt +++ /dev/null @@ -1,299 +0,0 @@ -package net.corda.plugins - -import com.typesafe.config.ConfigFactory -import com.typesafe.config.ConfigRenderOptions -import com.typesafe.config.ConfigValueFactory -import com.typesafe.config.ConfigObject -import groovy.lang.Closure -import net.corda.cordform.CordformNode -import net.corda.cordform.RpcSettings -import org.gradle.api.Project -import java.io.File -import java.nio.charset.StandardCharsets -import java.nio.file.Files -import java.nio.file.Path - -/** - * Represents a node that will be installed. - */ -class Node(private val project: Project) : CordformNode() { - companion object { - @JvmStatic - val webJarName = "corda-webserver.jar" - private val configFileProperty = "configFile" - } - - /** - * Set the list of CorDapps to install to the plugins directory. Each cordapp is a fully qualified Maven - * dependency name, eg: com.example:product-name:0.1 - * - * @note Your app will be installed by default and does not need to be included here. - * @note Type is any due to gradle's use of "GStrings" - each value will have "toString" called on it - */ - var cordapps = mutableListOf() - internal var additionalCordapps = mutableListOf() - internal lateinit var nodeDir: File - private set - internal lateinit var rootDir: File - private set - internal lateinit var containerName: String - private set - internal var rpcSettings: RpcSettings = RpcSettings() - private set - internal var webserverJar: String? = null - private set - - /** - * Sets whether this node will use HTTPS communication. - * - * @param isHttps True if this node uses HTTPS communication. - */ - fun https(isHttps: Boolean) { - config = config.withValue("useHTTPS", ConfigValueFactory.fromAnyRef(isHttps)) - } - - /** - * Sets the H2 port for this node - */ - fun h2Port(h2Port: Int) { - config = config.withValue("h2port", ConfigValueFactory.fromAnyRef(h2Port)) - } - - fun useTestClock(useTestClock: Boolean) { - config = config.withValue("useTestClock", ConfigValueFactory.fromAnyRef(useTestClock)) - } - - /** - * Specifies RPC settings for the node. - */ - fun rpcSettings(configureClosure: Closure) { - rpcSettings = project.configure(RpcSettings(), configureClosure) as RpcSettings - config = rpcSettings.addTo("rpcSettings", config) - } - - /** - * Enables SSH access on given port - * - * @param sshdPort The port for SSH server to listen on - */ - fun sshdPort(sshdPort: Int?) { - config = config.withValue("sshd.port", ConfigValueFactory.fromAnyRef(sshdPort)) - } - - /** - * The webserver JAR to be used by this node. - * - * If not provided, the default development webserver is used. - * - * @param webserverJar The file path of the webserver JAR to use. - */ - fun webserverJar(webserverJar: String) { - this.webserverJar = webserverJar - } - - internal fun build() { - if (config.hasPath("webAddress")) { - installWebserverJar() - } - installAgentJar() - installBuiltCordapp() - installCordapps() - } - - internal fun buildDocker() { - project.copy { - it.apply { - from(Cordformation.getPluginFile(project, "net/corda/plugins/Dockerfile")) - from(Cordformation.getPluginFile(project, "net/corda/plugins/run-corda.sh")) - into("$nodeDir/") - } - } - installAgentJar() - installBuiltCordapp() - installCordapps() - } - - internal fun rootDir(rootDir: Path) { - if (name == null) { - project.logger.error("Node has a null name - cannot create node") - throw IllegalStateException("Node has a null name - cannot create node") - } - // Parsing O= part directly because importing BouncyCastle provider in Cordformation causes problems - // with loading our custom X509EdDSAEngine. - val organizationName = name.trim().split(",").firstOrNull { it.startsWith("O=") }?.substringAfter("=") - val dirName = organizationName ?: name - containerName = dirName.replace("\\s+".toRegex(), "-").toLowerCase() - this.rootDir = rootDir.toFile() - nodeDir = File(this.rootDir, dirName.replace("\\s", "")) - Files.createDirectories(nodeDir.toPath()) - } - - private fun configureProperties() { - config = config.withValue("rpcUsers", ConfigValueFactory.fromIterable(rpcUsers)) - if (notary != null) { - config = config.withValue("notary", ConfigValueFactory.fromMap(notary)) - } - if (extraConfig != null) { - config = config.withFallback(ConfigFactory.parseMap(extraConfig)) - } - } - - /** - * Installs the corda webserver JAR to the node directory - */ - private fun installWebserverJar() { - // If no webserver JAR is provided, the default development webserver is used. - val webJar = if (webserverJar == null) { - project.logger.info("Using default development webserver.") - Cordformation.verifyAndGetRuntimeJar(project, "corda-webserver") - } else { - project.logger.info("Using custom webserver: $webserverJar.") - File(webserverJar) - } - - project.copy { - it.apply { - from(webJar) - into(nodeDir) - rename(webJar.name, webJarName) - } - } - } - - /** - * Installs this project's cordapp to this directory. - */ - private fun installBuiltCordapp() { - val cordappsDir = File(nodeDir, "cordapps") - project.copy { - it.apply { - from(project.tasks.getByName("jar")) - into(cordappsDir) - } - } - } - - /** - * Installs the jolokia monitoring agent JAR to the node/drivers directory - */ - private fun installAgentJar() { - // TODO: improve how we re-use existing declared external variables from root gradle.build - val jolokiaVersion = try { project.rootProject.ext("jolokia_version") } catch (e: Exception) { "1.3.7" } - val agentJar = project.configuration("runtime").files { - (it.group == "org.jolokia") && - (it.name == "jolokia-jvm") && - (it.version == jolokiaVersion) - // TODO: revisit when classifier attribute is added. eg && (it.classifier = "agent") - }.first() // should always be the jolokia agent fat jar: eg. jolokia-jvm-1.3.7-agent.jar - project.logger.info("Jolokia agent jar: $agentJar") - if (agentJar.isFile) { - val driversDir = File(nodeDir, "drivers") - project.copy { - it.apply { - from(agentJar) - into(driversDir) - } - } - } - } - - private fun createTempConfigFile(configObject: ConfigObject): File { - val options = ConfigRenderOptions - .defaults() - .setOriginComments(false) - .setComments(false) - .setFormatted(true) - .setJson(false) - val configFileText = configObject.render(options).split("\n").toList() - // Need to write a temporary file first to use the project.copy, which resolves directories correctly. - val tmpDir = File(project.buildDir, "tmp") - Files.createDirectories(tmpDir.toPath()) - var fileName = "${nodeDir.name}.conf" - val tmpConfFile = File(tmpDir, fileName) - Files.write(tmpConfFile.toPath(), configFileText, StandardCharsets.UTF_8) - return tmpConfFile - } - - /** - * Installs the configuration file to the root directory and detokenises it. - */ - internal fun installConfig() { - configureProperties() - val tmpConfFile = createTempConfigFile(config.root()) - appendOptionalConfig(tmpConfFile) - project.copy { - it.apply { - from(tmpConfFile) - into(rootDir) - } - } - } - - /** - * Installs the Dockerized configuration file to the root directory and detokenises it. - */ - internal fun installDockerConfig() { - configureProperties() - val dockerConf = config - .withValue("p2pAddress", ConfigValueFactory.fromAnyRef("$containerName:$p2pPort")) - .withValue("rpcSettings.address", ConfigValueFactory.fromAnyRef("$containerName:${rpcSettings.port}")) - .withValue("rpcSettings.adminAddress", ConfigValueFactory.fromAnyRef("$containerName:${rpcSettings.adminPort}")) - .withValue("detectPublicIp", ConfigValueFactory.fromAnyRef(false)) - val tmpConfFile = createTempConfigFile(dockerConf.root()) - appendOptionalConfig(tmpConfFile) - project.copy { - it.apply { - from(tmpConfFile) - into(rootDir) - } - } - } - - /** - * Appends installed config file with properties from an optional file. - */ - private fun appendOptionalConfig(confFile: File) { - val optionalConfig: File? = when { - project.findProperty(configFileProperty) != null -> //provided by -PconfigFile command line property when running Gradle task - File(project.findProperty(configFileProperty) as String) - config.hasPath(configFileProperty) -> File(config.getString(configFileProperty)) - else -> null - } - - if (optionalConfig != null) { - if (!optionalConfig.exists()) { - project.logger.error("$configFileProperty '$optionalConfig' not found") - } else { - confFile.appendBytes(optionalConfig.readBytes()) - } - } - } - - /** - * Installs other cordapps to this node's cordapps directory. - */ - internal fun installCordapps() { - additionalCordapps.addAll(getCordappList()) - val cordappsDir = File(nodeDir, "cordapps") - project.copy { - it.apply { - from(additionalCordapps) - into(cordappsDir) - } - } - } - - /** - * Gets a list of cordapps based on what dependent cordapps were specified. - * - * @return List of this node's cordapps. - */ - private fun getCordappList(): Collection { - // Cordapps can sometimes contain a GString instance which fails the equality test with the Java string - @Suppress("RemoveRedundantCallsOfConversionMethods") - val cordapps: List = cordapps.map { it.toString() } - return project.configuration("cordapp").files { - cordapps.contains(it.group + ":" + it.name + ":" + it.version) - } - } -} diff --git a/gradle-plugins/cordformation/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.cordformation.properties b/gradle-plugins/cordformation/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.cordformation.properties deleted file mode 100644 index 4475d5c692..0000000000 --- a/gradle-plugins/cordformation/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.cordformation.properties +++ /dev/null @@ -1 +0,0 @@ -implementation-class=net.corda.plugins.Cordformation diff --git a/gradle-plugins/cordformation/src/main/resources/net/corda/plugins/Dockerfile b/gradle-plugins/cordformation/src/main/resources/net/corda/plugins/Dockerfile deleted file mode 100644 index 66ab277852..0000000000 --- a/gradle-plugins/cordformation/src/main/resources/net/corda/plugins/Dockerfile +++ /dev/null @@ -1,44 +0,0 @@ -# Base image from (http://phusion.github.io/baseimage-docker) -FROM openjdk:8u151-jre-alpine - -ENV CORDA_VERSION=${BUILDTIME_CORDA_VERSION} -ENV JAVA_OPTIONS=${BUILDTIME_JAVA_OPTIONS} - -# Set image labels -LABEL net.corda.version = ${CORDA_VERSION} \ - maintainer = "" \ - vendor = "R3" - -RUN apk upgrade --update && \ - apk add --update --no-cache bash iputils && \ - rm -rf /var/cache/apk/* && \ - # Add user to run the app && \ - addgroup corda && \ - adduser -G corda -D -s /bin/bash corda && \ - # Create /opt/corda directory && \ - mkdir -p /opt/corda/plugins && \ - mkdir -p /opt/corda/logs - -# Copy corda files -ADD --chown=corda:corda corda.jar /opt/corda/corda.jar -ADD --chown=corda:corda node.conf /opt/corda/node.conf -ADD --chown=corda:corda network-parameters /opt/corda/ -ADD --chown=corda:corda cordapps/ /opt/corda/cordapps -ADD --chown=corda:corda additional-node-infos/ /opt/corda/additional-node-infos -ADD --chown=corda:corda certificates/ /opt/corda/certificates -ADD --chown=corda:corda drivers/ /opt/corda/drivers -ADD --chown=corda:corda persistence* /opt/corda/ - -COPY run-corda.sh /run-corda.sh - -RUN chmod +x /run-corda.sh && \ - sync && \ - chown -R corda:corda /opt/corda - -# Working directory for Corda -WORKDIR /opt/corda -ENV HOME=/opt/corda -USER corda - -# Start it -CMD ["/run-corda.sh"] \ No newline at end of file diff --git a/gradle-plugins/cordformation/src/main/resources/net/corda/plugins/run-corda.sh b/gradle-plugins/cordformation/src/main/resources/net/corda/plugins/run-corda.sh deleted file mode 100644 index c10604f859..0000000000 --- a/gradle-plugins/cordformation/src/main/resources/net/corda/plugins/run-corda.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh - -# If variable not present use default values -: ${CORDA_HOME:=/opt/corda} -: ${JAVA_OPTIONS:=-Xmx512m} - -export CORDA_HOME JAVA_OPTIONS - -cd ${CORDA_HOME} -java $JAVA_OPTIONS -jar ${CORDA_HOME}/corda.jar 2>&1 \ No newline at end of file diff --git a/gradle-plugins/cordformation/src/main/resources/net/corda/plugins/runnodes b/gradle-plugins/cordformation/src/main/resources/net/corda/plugins/runnodes deleted file mode 100644 index 9e3ba4c5be..0000000000 --- a/gradle-plugins/cordformation/src/main/resources/net/corda/plugins/runnodes +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -# Allow the script to be run from outside the nodes directory. -basedir=$( dirname "$0" ) -cd "$basedir" - -if which osascript >/dev/null; then - /usr/libexec/java_home -v 1.8 --exec java -jar runnodes.jar "$@" -else - "${JAVA_HOME:+$JAVA_HOME/bin/}java" -jar runnodes.jar "$@" -fi diff --git a/gradle-plugins/cordformation/src/main/resources/net/corda/plugins/runnodes.bat b/gradle-plugins/cordformation/src/main/resources/net/corda/plugins/runnodes.bat deleted file mode 100644 index a6acf1f737..0000000000 --- a/gradle-plugins/cordformation/src/main/resources/net/corda/plugins/runnodes.bat +++ /dev/null @@ -1,8 +0,0 @@ -@echo off - -REM Change to the directory of this script (%~dp0) -Pushd %~dp0 - -java -jar runnodes.jar %* - -Popd \ No newline at end of file diff --git a/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt b/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt deleted file mode 100644 index bb308a8191..0000000000 --- a/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt +++ /dev/null @@ -1,151 +0,0 @@ -package net.corda.plugins - -import java.awt.GraphicsEnvironment -import java.io.File -import java.nio.file.Files -import java.util.* - -private val HEADLESS_FLAG = "--headless" -private val CAPSULE_DEBUG_FLAG = "--capsule-debug" - -private val os by lazy { - val osName = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH) - if ("mac" in osName || "darwin" in osName) OS.MACOS - else if ("win" in osName) OS.WINDOWS - else OS.LINUX -} - -private enum class OS { MACOS, WINDOWS, LINUX } - -private object debugPortAlloc { - private var basePort = 5005 - internal fun next() = basePort++ -} - -private object monitoringPortAlloc { - private var basePort = 7005 - internal fun next() = basePort++ -} - -fun main(args: Array) { - val startedProcesses = mutableListOf() - val headless = GraphicsEnvironment.isHeadless() || args.contains(HEADLESS_FLAG) - val capsuleDebugMode = args.contains(CAPSULE_DEBUG_FLAG) - val workingDir = File(System.getProperty("user.dir")) - val javaArgs = args.filter { it != HEADLESS_FLAG && it != CAPSULE_DEBUG_FLAG } - val jvmArgs = if (capsuleDebugMode) listOf("-Dcapsule.log=verbose") else emptyList() - println("Starting nodes in $workingDir") - workingDir.listFiles { file -> file.isDirectory }.forEach { dir -> - listOf(NodeJarType, WebJarType).forEach { jarType -> - jarType.acceptDirAndStartProcess(dir, headless, javaArgs, jvmArgs)?.let { startedProcesses += it } - } - } - println("Started ${startedProcesses.size} processes") - println("Finished starting nodes") -} - -private abstract class JarType(private val jarName: String) { - internal abstract fun acceptNodeConf(nodeConf: File): Boolean - internal fun acceptDirAndStartProcess(dir: File, headless: Boolean, javaArgs: List, jvmArgs: List): Process? { - if (!File(dir, jarName).exists()) { - return null - } - if (!File(dir, "node.conf").let { it.exists() && acceptNodeConf(it) }) { - return null - } - val debugPort = debugPortAlloc.next() - val monitoringPort = monitoringPortAlloc.next() - println("Starting $jarName in $dir on debug port $debugPort") - val process = (if (headless) ::HeadlessJavaCommand else ::TerminalWindowJavaCommand)(jarName, dir, debugPort, monitoringPort, javaArgs, jvmArgs).start() - if (os == OS.MACOS) Thread.sleep(1000) - return process - } -} - -private object NodeJarType : JarType("corda.jar") { - override fun acceptNodeConf(nodeConf: File) = true -} - -private object WebJarType : JarType("corda-webserver.jar") { - // TODO: Add a webserver.conf, or use TypeSafe config instead of this hack - override fun acceptNodeConf(nodeConf: File) = Files.lines(nodeConf.toPath()).anyMatch { "webAddress" in it } -} - -private abstract class JavaCommand( - jarName: String, - internal val dir: File, - debugPort: Int?, - monitoringPort: Int?, - internal val nodeName: String, - init: MutableList.() -> Unit, args: List, - jvmArgs: List -) { - private val jolokiaJar by lazy { - File("$dir/drivers").listFiles { _, filename -> - filename.matches("jolokia-jvm-.*-agent\\.jar$".toRegex()) - }.first().name - } - - internal val command: List = mutableListOf().apply { - add(getJavaPath()) - addAll(jvmArgs) - add("-Dname=$nodeName") - val jvmArgs: MutableList = mutableListOf() - null != debugPort && jvmArgs.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$debugPort") - null != monitoringPort && jvmArgs.add("-javaagent:drivers/$jolokiaJar=port=$monitoringPort") - if (jvmArgs.isNotEmpty()) { - add("-Dcapsule.jvm.args=${jvmArgs.joinToString(separator = " ")}") - } - add("-jar") - add(jarName) - init() - addAll(args) - } - - internal abstract fun processBuilder(): ProcessBuilder - internal fun start() = processBuilder().directory(dir).start() - internal abstract fun getJavaPath(): String -} - -private class HeadlessJavaCommand(jarName: String, dir: File, debugPort: Int?, monitoringPort: Int?, args: List, jvmArgs: List) - : JavaCommand(jarName, dir, debugPort, monitoringPort, dir.name, { add("--no-local-shell") }, args, jvmArgs) { - override fun processBuilder() = ProcessBuilder(command).redirectError(File("error.$nodeName.log")).inheritIO() - override fun getJavaPath() = File(File(System.getProperty("java.home"), "bin"), "java").path -} - -private class TerminalWindowJavaCommand(jarName: String, dir: File, debugPort: Int?, monitoringPort: Int?, args: List, jvmArgs: List) - : JavaCommand(jarName, dir, debugPort, monitoringPort, "${dir.name}-$jarName", {}, args, jvmArgs) { - override fun processBuilder() = ProcessBuilder(when (os) { - OS.MACOS -> { - listOf("osascript", "-e", """tell app "Terminal" - activate - delay 0.5 - tell app "System Events" to tell process "Terminal" to keystroke "t" using command down - delay 0.5 - do script "bash -c 'cd \"$dir\" ; \"${command.joinToString("""\" \"""")}\" && exit'" in selected tab of the front window -end tell""") - } - OS.WINDOWS -> { - listOf("cmd", "/C", "start ${command.joinToString(" ") { windowsSpaceEscape(it) }}") - } - OS.LINUX -> { - // Start shell to keep window open unless java terminated normally or due to SIGTERM: - val command = "${unixCommand()}; [ $? -eq 0 -o $? -eq 143 ] || sh" - if (isTmux()) { - listOf("tmux", "new-window", "-n", nodeName, command) - } else { - listOf("xterm", "-T", nodeName, "-e", command) - } - } - }) - - private fun unixCommand() = command.map(::quotedFormOf).joinToString(" ") - override fun getJavaPath(): String = File(File(System.getProperty("java.home"), "bin"), "java").path - - // Replace below is to fix an issue with spaces in paths on Windows. - // Quoting the entire path does not work, only the space or directory within the path. - private fun windowsSpaceEscape(s:String) = s.replace(" ", "\" \"") -} - -private fun quotedFormOf(text: String) = "'${text.replace("'", "'\\''")}'" // Suitable for UNIX shells. -private fun isTmux() = System.getenv("TMUX")?.isNotEmpty() ?: false diff --git a/gradle-plugins/publish-utils/README.rst b/gradle-plugins/publish-utils/README.rst deleted file mode 100644 index d1657ee5fe..0000000000 --- a/gradle-plugins/publish-utils/README.rst +++ /dev/null @@ -1,92 +0,0 @@ -Publish Utils -============= - -Publishing utilities adds a couple of tasks to any project it is applied to that hide some boilerplate that would -otherwise be placed in the Cordapp template's build.gradle. - -There are two tasks exposed: `sourceJar` and `javadocJar` and both return a `FileCollection`. - -It is used within the `publishing` block of a build.gradle as such; - -.. code-block:: text - - // This will publish the sources, javadoc, and Java components to Maven. - // See the `maven-publish` plugin for more info: https://docs.gradle.org/current/userguide/publishing_maven.html - publishing { - publications { - jarAndSources(MavenPublication) { - from components.java - // The two lines below are the tasks added by this plugin. - artifact sourceJar - artifact javadocJar - } - } - } - -Bintray Publishing ------------------- - -For large multibuild projects it can be inconvenient to store the entire configuration for bintray and maven central -per project (with a bintray and publishing block with extended POM information). Publish utils can bring the number of -configuration blocks down to one in the ideal scenario. - -To use this plugin you must first apply it to both the root project and any project that will be published with - -.. code-block:: text - - apply plugin: 'net.corda.plugins.publish-utils' - -Next you must setup the general bintray configuration you wish to use project wide, for example: - -.. code-block:: text - - bintrayConfig { - user = - key = - repo = 'example repo' - org = 'example organisation' - licenses = ['a license'] - vcsUrl = 'https://example.com' - projectUrl = 'https://example.com' - gpgSign = true // Whether to GPG sign - gpgPassphrase = // Only required if gpgSign is true and your key is passworded - publications = ['example'] // a list of publications (see below) - license { - name = 'example' - url = 'https://example.com' - distribution = 'repo' - } - developer { - id = 'a developer id' - name = 'a developer name' - email = 'example@example.com' - } - } - -.. note:: You can currently only have one license and developer in the maven POM sections - -**Publications** - -This plugin assumes, by default, that publications match the name of the project. This means, by default, you can -just list the names of the projects you wish to publish (e.g. to publish `test:myapp` you need `publications = ['myapp']`. -If a project requires a different name you can configure it *per project* with the project configuration block. - -The project configuration block has the following structure: - -.. code-block:: text - - publish { - disableDefaultJar = false // set to true to disable the default JAR being created (e.g. when creating a fat JAR) - name 'non-default-project-name' // Always put this last because it causes configuration to happen - } - -**Artifacts** - -To add additional artifacts to the project you can use the default gradle `artifacts` block with the `publish` -configuration. For example: - - artifacts { - publish buildFatJar { - // You can configure this as a regular maven publication - } - } diff --git a/gradle-plugins/publish-utils/build.gradle b/gradle-plugins/publish-utils/build.gradle deleted file mode 100644 index da9c659498..0000000000 --- a/gradle-plugins/publish-utils/build.gradle +++ /dev/null @@ -1,109 +0,0 @@ -apply plugin: 'groovy' -apply plugin: 'maven-publish' -apply plugin: 'com.jfrog.bintray' -apply plugin: 'com.jfrog.artifactory' - -// Used for bootstrapping project -buildscript { - Properties constants = new Properties() - file("../../constants.properties").withInputStream { constants.load(it) } - - ext { - gradle_plugins_version = constants.getProperty("gradlePluginsVersion") - artifactory_plugin_version = constants.getProperty('artifactoryPluginVersion') - } - - repositories { - jcenter() - } - - dependencies { - classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.4' - classpath "org.jfrog.buildinfo:build-info-extractor-gradle:$artifactory_plugin_version" - } -} - -version "$gradle_plugins_version" - -dependencies { - compile gradleApi() - compile localGroovy() -} - -repositories { - mavenCentral() -} - -task("sourceJar", type: Jar, dependsOn: classes) { - classifier = 'sources' - from sourceSets.main.allSource -} - -task("javadocJar", type: Jar, dependsOn: javadoc) { - classifier = 'javadoc' - from javadoc.destinationDir -} - -bintray { - user = System.getenv('CORDA_BINTRAY_USER') - key = System.getenv('CORDA_BINTRAY_KEY') - publications = ['publishUtils'] - dryRun = false - pkg { - repo = 'corda' - name = 'publish-utils' - userOrg = 'r3' - licenses = ['Apache-2.0'] - - version { - gpg { - sign = true - passphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE') - } - } - } -} - -publishing { - publications { - publishUtils(MavenPublication) { - from components.java - groupId 'net.corda.plugins' - artifactId 'publish-utils' - - artifact sourceJar - artifact javadocJar - - pom.withXml { - asNode().children().last() + { - resolveStrategy = Closure.DELEGATE_FIRST - name 'publish-utils' - description 'A small gradle plugin that adds a couple of convenience functions for publishing to Maven' - url 'https://github.com/corda/corda' - scm { - url 'https://github.com/corda/corda' - } - - licenses { - license { - name 'Apache-2.0' - url 'https://www.apache.org/licenses/LICENSE-2.0' - distribution 'repo' - } - } - - developers { - developer { - id 'R3' - name 'R3' - email 'dev@corda.net' - } - } - } - } - } - } -} - -// Aliasing the publishToMavenLocal for simplicity. -task(install, dependsOn: 'publishToMavenLocal') diff --git a/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/ProjectPublishExtension.groovy b/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/ProjectPublishExtension.groovy deleted file mode 100644 index edb543fa51..0000000000 --- a/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/ProjectPublishExtension.groovy +++ /dev/null @@ -1,39 +0,0 @@ -package net.corda.plugins - -class ProjectPublishExtension { - private PublishTasks task - - void setPublishTask(PublishTasks task) { - this.task = task - } - - /** - * Use a different name from the current project name for publishing. - * Set this after all other settings that need to be configured - */ - void name(String name) { - task.setPublishName(name) - } - - /** - * Get the publishing name for this project. - */ - String name() { - return task.getPublishName() - } - - /** - * True when we do not want to publish default Java components - */ - Boolean disableDefaultJar = false - - /** - * True if publishing a WAR instead of a JAR. Forces disableDefaultJAR to "true" when true - */ - Boolean publishWar = false - - /** - * True if publishing sources to remote repositories - */ - Boolean publishSources = true -} \ No newline at end of file diff --git a/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/PublishTasks.groovy b/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/PublishTasks.groovy deleted file mode 100644 index 772ac99d23..0000000000 --- a/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/PublishTasks.groovy +++ /dev/null @@ -1,167 +0,0 @@ -package net.corda.plugins - -import org.gradle.api.* -import org.gradle.api.tasks.bundling.Jar -import org.gradle.api.tasks.javadoc.Javadoc -import org.gradle.api.Project -import org.gradle.api.publish.maven.MavenPublication -import org.gradle.api.publish.maven.MavenPom -import net.corda.plugins.bintray.* - -/** - * A utility plugin that when applied will automatically create source and javadoc publishing tasks - * To apply this plugin you must also add 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.4' to your - * buildscript's classpath dependencies. - * - * To use this plugin you can add a new configuration block (extension) to your root build.gradle. See the fields - * in BintrayConfigExtension. - */ -class PublishTasks implements Plugin { - Project project - String publishName - ProjectPublishExtension publishConfig - - void apply(Project project) { - this.project = project - this.publishName = project.name - - createTasks() - createExtensions() - createConfigurations() - } - - /** - * This call must come at the end of any publish block because it configures the publishing and any - * values set after this call in the DSL will not be configured properly (and will use the default value) - */ - void setPublishName(String publishName) { - project.logger.info("Changing publishing name from ${project.name} to ${publishName}") - this.publishName = publishName - checkAndConfigurePublishing() - } - - void checkAndConfigurePublishing() { - project.logger.info("Checking whether to publish $publishName") - def bintrayConfig = project.rootProject.extensions.findByType(BintrayConfigExtension.class) - if((bintrayConfig != null) && (bintrayConfig.publications) && (bintrayConfig.publications.findAll { it == publishName }.size() > 0)) { - configurePublishing(bintrayConfig) - } - } - - void configurePublishing(BintrayConfigExtension bintrayConfig) { - project.logger.info("Configuring bintray for ${publishName}") - configureMavenPublish(bintrayConfig) - configureBintray(bintrayConfig) - } - - void configureMavenPublish(BintrayConfigExtension bintrayConfig) { - project.logger.info("Configuring maven publish for $publishName") - project.apply([plugin: 'maven-publish']) - project.publishing.publications.create(publishName, MavenPublication) { - groupId project.group - artifactId publishName - - if (publishConfig.publishSources) { - project.logger.info("Publishing sources for $publishName") - artifact project.tasks.sourceJar - } - artifact project.tasks.javadocJar - - project.configurations.publish.artifacts.each { - project.logger.debug("Adding artifact: $it") - delegate.artifact it - } - - if (!publishConfig.disableDefaultJar && !publishConfig.publishWar) { - from project.components.java - } else if (publishConfig.publishWar) { - from project.components.web - } - - extendPomForMavenCentral(pom, bintrayConfig) - } - project.task("install", dependsOn: "publishToMavenLocal") - } - - // Maven central requires all of the below fields for this to be a valid POM - void extendPomForMavenCentral(MavenPom pom, BintrayConfigExtension config) { - pom.withXml { - asNode().children().last() + { - resolveStrategy = Closure.DELEGATE_FIRST - name publishName - description project.description - url config.projectUrl - scm { - url config.vcsUrl - } - - licenses { - license { - name config.license.name - url config.license.url - distribution config.license.url - } - } - - developers { - developer { - id config.developer.id - name config.developer.name - email config.developer.email - } - } - } - } - } - - void configureBintray(BintrayConfigExtension bintrayConfig) { - project.apply([plugin: 'com.jfrog.bintray']) - project.bintray { - user = bintrayConfig.user - key = bintrayConfig.key - publications = [ publishName ] - dryRun = bintrayConfig.dryRun ?: false - pkg { - repo = bintrayConfig.repo - name = publishName - userOrg = bintrayConfig.org - licenses = bintrayConfig.licenses - - version { - gpg { - sign = bintrayConfig.gpgSign ?: false - passphrase = bintrayConfig.gpgPassphrase - } - } - } - } - } - - void createTasks() { - if(project.hasProperty('classes')) { - project.task("sourceJar", type: Jar, dependsOn: project.classes) { - classifier = 'sources' - from project.sourceSets.main.allSource - } - } - - if(project.hasProperty('javadoc')) { - project.task("javadocJar", type: Jar, dependsOn: project.javadoc) { - classifier = 'javadoc' - from project.javadoc.destinationDir - } - } - } - - void createExtensions() { - if(project == project.rootProject) { - project.extensions.create("bintrayConfig", BintrayConfigExtension) - } - publishConfig = project.extensions.create("publish", ProjectPublishExtension) - publishConfig.setPublishTask(this) - } - - void createConfigurations() { - project.configurations.create("publish") - } -} diff --git a/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/bintray/BintrayConfigExtension.groovy b/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/bintray/BintrayConfigExtension.groovy deleted file mode 100644 index 1a1c4e49e5..0000000000 --- a/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/bintray/BintrayConfigExtension.groovy +++ /dev/null @@ -1,70 +0,0 @@ -package net.corda.plugins.bintray - -import org.gradle.util.ConfigureUtil - -class BintrayConfigExtension { - /** - * Bintray username - */ - String user - /** - * Bintray access key - */ - String key - /** - * Bintray repository - */ - String repo - /** - * Bintray organisation - */ - String org - /** - * Licenses for packages uploaded by this configuration - */ - String[] licenses - /** - * Whether to sign packages uploaded by this configuration - */ - Boolean gpgSign - /** - * The passphrase for the key used to sign releases. - */ - String gpgPassphrase - /** - * VCS URL - */ - String vcsUrl - /** - * Project URL - */ - String projectUrl - /** - * The publications that will be uploaded as a part of this configuration. These must match both the name on - * bintray and the gradle module name. ie; it must be "some-package" as a gradle sub-module (root project not - * supported, this extension is to improve multi-build bintray uploads). The publication must also be called - * "some-package". Only one publication can be uploaded per module (a bintray plugin restriction(. - * If any of these conditions are not met your package will not be uploaded. - */ - String[] publications - /** - * Whether to test the publication without uploading to bintray. - */ - Boolean dryRun - /** - * The license this project will use (currently limited to one) - */ - License license = new License() - /** - * The developer of this project (currently limited to one) - */ - Developer developer = new Developer() - - void license(Closure closure) { - ConfigureUtil.configure(closure, license) - } - - void developer(Closure closure) { - ConfigureUtil.configure(closure, developer) - } -} \ No newline at end of file diff --git a/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/bintray/Developer.groovy b/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/bintray/Developer.groovy deleted file mode 100644 index 1d66f68c7d..0000000000 --- a/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/bintray/Developer.groovy +++ /dev/null @@ -1,16 +0,0 @@ -package net.corda.plugins.bintray - -class Developer { - /** - * A unique identifier the developer (eg; organisation ID) - */ - String id - /** - * The full name of the developer - */ - String name - /** - * An email address for contacting the developer - */ - String email -} \ No newline at end of file diff --git a/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/bintray/License.groovy b/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/bintray/License.groovy deleted file mode 100644 index 1d06867bcf..0000000000 --- a/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/bintray/License.groovy +++ /dev/null @@ -1,16 +0,0 @@ -package net.corda.plugins.bintray - -class License { - /** - * The name of license (eg; Apache 2.0) - */ - String name - /** - * URL to the full license file - */ - String url - /** - * The distribution level this license corresponds to (eg: repo) - */ - String distribution -} \ No newline at end of file diff --git a/gradle-plugins/publish-utils/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.publish-utils.properties b/gradle-plugins/publish-utils/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.publish-utils.properties deleted file mode 100644 index b680f7d301..0000000000 --- a/gradle-plugins/publish-utils/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.publish-utils.properties +++ /dev/null @@ -1 +0,0 @@ -implementation-class=net.corda.plugins.PublishTasks diff --git a/gradle-plugins/quasar-utils/README.rst b/gradle-plugins/quasar-utils/README.rst deleted file mode 100644 index 481d8ec66b..0000000000 --- a/gradle-plugins/quasar-utils/README.rst +++ /dev/null @@ -1,4 +0,0 @@ -Quasar Utils -============ - -Quasar utilities adds several tasks and configuration that provide a default Quasar setup and removes some boilerplate. \ No newline at end of file diff --git a/gradle-plugins/quasar-utils/build.gradle b/gradle-plugins/quasar-utils/build.gradle deleted file mode 100644 index 8f9eca30f2..0000000000 --- a/gradle-plugins/quasar-utils/build.gradle +++ /dev/null @@ -1,19 +0,0 @@ -apply plugin: 'groovy' -apply plugin: 'maven-publish' -apply plugin: 'net.corda.plugins.publish-utils' -apply plugin: 'com.jfrog.artifactory' - -description 'A small gradle plugin for adding some basic Quasar tasks and configurations to reduce build.gradle bloat.' - -repositories { - mavenCentral() -} - -dependencies { - compile gradleApi() - compile localGroovy() -} - -publish { - name project.name -} diff --git a/gradle-plugins/quasar-utils/src/main/groovy/net/corda/plugins/QuasarPlugin.groovy b/gradle-plugins/quasar-utils/src/main/groovy/net/corda/plugins/QuasarPlugin.groovy deleted file mode 100644 index 6a4ffaf25a..0000000000 --- a/gradle-plugins/quasar-utils/src/main/groovy/net/corda/plugins/QuasarPlugin.groovy +++ /dev/null @@ -1,28 +0,0 @@ -package net.corda.plugins - -import org.gradle.api.Project -import org.gradle.api.Plugin -import org.gradle.api.tasks.testing.Test -import org.gradle.api.tasks.JavaExec - -/** - * QuasarPlugin creates a "quasar" configuration and adds quasar as a dependency. - */ -class QuasarPlugin implements Plugin { - void apply(Project project) { - project.configurations.create("quasar") -// To add a local .jar dependency: -// project.dependencies.add("quasar", project.files("${project.rootProject.projectDir}/lib/quasar.jar")) - project.dependencies.add("quasar", "co.paralleluniverse:quasar-core:${project.rootProject.ext.quasar_version}:jdk8@jar") - project.dependencies.add("runtime", project.configurations.getByName("quasar")) - - project.tasks.withType(Test) { - jvmArgs "-javaagent:${project.configurations.quasar.singleFile}" - jvmArgs "-Dco.paralleluniverse.fibers.verifyInstrumentation" - } - project.tasks.withType(JavaExec) { - jvmArgs "-javaagent:${project.configurations.quasar.singleFile}" - jvmArgs "-Dco.paralleluniverse.fibers.verifyInstrumentation" - } - } -} diff --git a/gradle-plugins/quasar-utils/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.quasar-utils.properties b/gradle-plugins/quasar-utils/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.quasar-utils.properties deleted file mode 100644 index fb7042bb42..0000000000 --- a/gradle-plugins/quasar-utils/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.quasar-utils.properties +++ /dev/null @@ -1 +0,0 @@ -implementation-class=net.corda.plugins.QuasarPlugin diff --git a/gradle-plugins/settings.gradle b/gradle-plugins/settings.gradle deleted file mode 100644 index 995cd8c899..0000000000 --- a/gradle-plugins/settings.gradle +++ /dev/null @@ -1,7 +0,0 @@ -rootProject.name = 'corda-gradle-plugins' -include 'publish-utils' -include 'quasar-utils' -include 'cordformation' -include 'cordform-common' -include 'api-scanner' -include 'cordapp' \ No newline at end of file diff --git a/lib/quasar.jar b/lib/quasar.jar index c9f0010e79..074ecc3b7c 100644 Binary files a/lib/quasar.jar and b/lib/quasar.jar differ diff --git a/node-api/build.gradle b/node-api/build.gradle index d2c51dde94..6cc03bd117 100644 --- a/node-api/build.gradle +++ b/node-api/build.gradle @@ -34,6 +34,12 @@ 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' + + // Pure-Java Snappy compression + compile 'org.iq80.snappy:snappy:0.4' + // Unit testing helpers. testCompile "junit:junit:$junit_version" testCompile "org.assertj:assertj-core:$assertj_version" diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt b/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt index 96bd8c0560..9392c176c7 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt @@ -10,6 +10,7 @@ import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.utilities.Id +import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.Try import org.apache.activemq.artemis.api.core.ActiveMQBuffer import org.apache.activemq.artemis.api.core.SimpleString @@ -72,6 +73,8 @@ object RPCApi { const val RPC_CLIENT_BINDING_ADDITIONS = "rpc.clientqueueadditions" const val RPC_TARGET_LEGAL_IDENTITY = "rpc-target-legal-identity" + const val DEDUPLICATION_SEQUENCE_NUMBER_FIELD_NAME = "deduplication-sequence-number" + val RPC_CLIENT_BINDING_REMOVAL_FILTER_EXPRESSION = "${ManagementHelper.HDR_NOTIFICATION_TYPE} = '${CoreNotificationType.BINDING_REMOVED.name}' AND " + "${ManagementHelper.HDR_ROUTING_NAME} LIKE '$RPC_CLIENT_QUEUE_NAME_PREFIX.%'" @@ -94,6 +97,8 @@ object RPCApi { OBSERVABLES_CLOSED } + abstract fun writeToClientMessage(message: ClientMessage) + /** * Request to a server to trigger the specified method with the provided arguments. * @@ -105,13 +110,13 @@ object RPCApi { data class RpcRequest( val clientAddress: SimpleString, val methodName: String, - val serialisedArguments: ByteArray, + val serialisedArguments: OpaqueBytes, val replyId: InvocationId, val sessionId: SessionId, val externalTrace: Trace? = null, val impersonatedActor: Actor? = null ) : ClientToServer() { - fun writeToClientMessage(message: ClientMessage) { + override fun writeToClientMessage(message: ClientMessage) { MessageUtil.setJMSReplyTo(message, clientAddress) message.putIntProperty(TAG_FIELD_NAME, Tag.RPC_REQUEST.ordinal) @@ -122,12 +127,12 @@ object RPCApi { impersonatedActor?.mapToImpersonated(message) message.putStringProperty(METHOD_NAME_FIELD_NAME, methodName) - message.bodyBuffer.writeBytes(serialisedArguments) + message.bodyBuffer.writeBytes(serialisedArguments.bytes) } } data class ObservablesClosed(val ids: List) : ClientToServer() { - fun writeToClientMessage(message: ClientMessage) { + override fun writeToClientMessage(message: ClientMessage) { message.putIntProperty(TAG_FIELD_NAME, Tag.OBSERVABLES_CLOSED.ordinal) val buffer = message.bodyBuffer buffer.writeInt(ids.size) @@ -144,7 +149,7 @@ object RPCApi { RPCApi.ClientToServer.Tag.RPC_REQUEST -> RpcRequest( clientAddress = MessageUtil.getJMSReplyTo(message), methodName = message.getStringProperty(METHOD_NAME_FIELD_NAME), - serialisedArguments = message.getBodyAsByteArray(), + serialisedArguments = OpaqueBytes(message.getBodyAsByteArray()), replyId = message.replyId(), sessionId = message.sessionId(), externalTrace = message.externalTrace(), @@ -175,13 +180,20 @@ object RPCApi { abstract fun writeToClientMessage(context: SerializationContext, message: ClientMessage) - /** Reply in response to an [ClientToServer.RpcRequest]. */ + /** The identity used to identify the deduplication ID sequence. This should be unique per server JVM run */ + abstract val deduplicationIdentity: String + + /** + * Reply in response to an [ClientToServer.RpcRequest]. + */ data class RpcReply( val id: InvocationId, - val result: Try + val result: Try, + override val deduplicationIdentity: String ) : ServerToClient() { override fun writeToClientMessage(context: SerializationContext, message: ClientMessage) { message.putIntProperty(TAG_FIELD_NAME, Tag.RPC_REPLY.ordinal) + message.putStringProperty(DEDUPLICATION_IDENTITY_FIELD_NAME, deduplicationIdentity) id.mapTo(message, RPC_ID_FIELD_NAME, RPC_ID_TIMESTAMP_FIELD_NAME) message.bodyBuffer.writeBytes(result.safeSerialize(context) { Try.Failure(it) }.bytes) } @@ -189,10 +201,12 @@ object RPCApi { data class Observation( val id: InvocationId, - val content: Notification<*> + val content: Notification<*>, + override val deduplicationIdentity: String ) : ServerToClient() { override fun writeToClientMessage(context: SerializationContext, message: ClientMessage) { message.putIntProperty(TAG_FIELD_NAME, Tag.OBSERVATION.ordinal) + message.putStringProperty(DEDUPLICATION_IDENTITY_FIELD_NAME, deduplicationIdentity) id.mapTo(message, OBSERVABLE_ID_FIELD_NAME, OBSERVABLE_ID_TIMESTAMP_FIELD_NAME) message.bodyBuffer.writeBytes(content.safeSerialize(context) { Notification.createOnError(it) }.bytes) } @@ -207,17 +221,26 @@ object RPCApi { fun fromClientMessage(context: SerializationContext, message: ClientMessage): ServerToClient { val tag = Tag.values()[message.getIntProperty(TAG_FIELD_NAME)] + val deduplicationIdentity = message.getStringProperty(DEDUPLICATION_IDENTITY_FIELD_NAME) return when (tag) { RPCApi.ServerToClient.Tag.RPC_REPLY -> { val id = message.invocationId(RPC_ID_FIELD_NAME, RPC_ID_TIMESTAMP_FIELD_NAME) ?: throw IllegalStateException("Cannot parse invocation id from client message.") val poolWithIdContext = context.withProperty(RpcRequestOrObservableIdKey, id) - RpcReply(id, message.getBodyAsByteArray().deserialize(context = poolWithIdContext)) + RpcReply( + id = id, + deduplicationIdentity = deduplicationIdentity, + result = message.getBodyAsByteArray().deserialize(context = poolWithIdContext) + ) } RPCApi.ServerToClient.Tag.OBSERVATION -> { val observableId = message.invocationId(OBSERVABLE_ID_FIELD_NAME, OBSERVABLE_ID_TIMESTAMP_FIELD_NAME) ?: throw IllegalStateException("Cannot parse invocation id from client message.") val poolWithIdContext = context.withProperty(RpcRequestOrObservableIdKey, observableId) val payload = message.getBodyAsByteArray().deserialize>(context = poolWithIdContext) - Observation(observableId, payload) + Observation( + id = observableId, + deduplicationIdentity = deduplicationIdentity, + content = payload + ) } } } @@ -225,18 +248,6 @@ object RPCApi { } } -data class ArtemisProducer( - val sessionFactory: ClientSessionFactory, - val session: ClientSession, - val producer: ClientProducer -) - -data class ArtemisConsumer( - val sessionFactory: ClientSessionFactory, - val session: ClientSession, - val consumer: ClientConsumer -) - private val TAG_FIELD_NAME = "tag" private val RPC_ID_FIELD_NAME = "rpc-id" private val RPC_ID_TIMESTAMP_FIELD_NAME = "rpc-id-timestamp" @@ -249,6 +260,7 @@ private val RPC_EXTERNAL_SESSION_ID_TIMESTAMP_FIELD_NAME = "rpc-external-session private val RPC_IMPERSONATED_ACTOR_ID = "rpc-impersonated-actor-id" private val RPC_IMPERSONATED_ACTOR_STORE_ID = "rpc-impersonated-actor-store-id" private val RPC_IMPERSONATED_ACTOR_OWNING_LEGAL_IDENTITY = "rpc-impersonated-actor-owningLegalIdentity" +private val DEDUPLICATION_IDENTITY_FIELD_NAME = "deduplication-identity" private val OBSERVABLE_ID_FIELD_NAME = "observable-id" private val OBSERVABLE_ID_TIMESTAMP_FIELD_NAME = "observable-id-timestamp" private val METHOD_NAME_FIELD_NAME = "method-name" diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingClient.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingClient.kt index 99b3ab8e38..4bbdf42d88 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingClient.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingClient.kt @@ -13,17 +13,25 @@ import org.apache.activemq.artemis.api.core.client.ClientProducer import org.apache.activemq.artemis.api.core.client.ClientSession import org.apache.activemq.artemis.api.core.client.ClientSessionFactory -class ArtemisMessagingClient(private val config: SSLConfiguration, private val serverAddress: NetworkHostAndPort, private val maxMessageSize: Int) { +interface ArtemisSessionProvider { + fun start(): ArtemisMessagingClient.Started + fun stop() + val started: ArtemisMessagingClient.Started? +} + +class ArtemisMessagingClient(private val config: SSLConfiguration, + private val serverAddress: NetworkHostAndPort, + private val maxMessageSize: Int) : ArtemisSessionProvider { companion object { private val log = loggerFor() } class Started(val sessionFactory: ClientSessionFactory, val session: ClientSession, val producer: ClientProducer) - var started: Started? = null + override var started: Started? = null private set - fun start(): Started = synchronized(this) { + override fun start(): Started = synchronized(this) { check(started == null) { "start can't be called twice" } log.info("Connecting to message broker: $serverAddress") // TODO Add broker CN to config for host verification in case the embedded broker isn't used @@ -48,7 +56,7 @@ class ArtemisMessagingClient(private val config: SSLConfiguration, private val s return Started(sessionFactory, session, producer).also { started = it } } - fun stop() = synchronized(this) { + override fun stop() = synchronized(this) { started?.run { producer.close() // Ensure any trailing messages are committed to the journal diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoader.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoader.kt index 20d2efca63..a499bbfc1b 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoader.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoader.kt @@ -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, 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) { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ClassloaderUtils.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ClassloaderUtils.kt new file mode 100644 index 0000000000..774e30e2c0 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ClassloaderUtils.kt @@ -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 { + 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 withContractsInJar(jarInputStream: InputStream, withContracts: (List, 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() + } +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/DeduplicationChecker.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/DeduplicationChecker.kt new file mode 100644 index 0000000000..b35b8922a3 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/DeduplicationChecker.kt @@ -0,0 +1,30 @@ +package net.corda.nodeapi.internal + +import com.google.common.cache.CacheBuilder +import com.google.common.cache.CacheLoader +import java.time.Duration +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicLong + +/** + * A class allowing the deduplication of a strictly incrementing sequence number. + */ +class DeduplicationChecker(cacheExpiry: Duration) { + // dedupe identity -> watermark cache + private val watermarkCache = CacheBuilder.newBuilder() + .expireAfterAccess(cacheExpiry.toNanos(), TimeUnit.NANOSECONDS) + .build(WatermarkCacheLoader) + + private object WatermarkCacheLoader : CacheLoader() { + override fun load(key: Any) = AtomicLong(-1) + } + + /** + * @param identity the identity that generates the sequence numbers. + * @param sequenceNumber the sequence number to check. + * @return true if the message is unique, false if it's a duplicate. + */ + fun checkDuplicateMessageId(identity: Any, sequenceNumber: Long): Boolean { + return watermarkCache[identity].getAndUpdate { maxOf(sequenceNumber, it) } >= sequenceNumber + } +} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/SignedNodeInfo.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/SignedNodeInfo.kt index d328674a69..3dea456a05 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/SignedNodeInfo.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/SignedNodeInfo.kt @@ -15,7 +15,6 @@ import java.security.SignatureException * A signed [NodeInfo] object containing a signature for each identity. The list of signatures is expected * to be in the same order as the identities. */ -// TODO Move this to net.corda.nodeapi.internal.network // TODO Add signatures for composite keys. The current thinking is to make sure there is a signature for each leaf key // that the node owns. This check can only be done by the network map server as it can check with the doorman if a node // is part of a composite identity. This of course further requires the doorman being able to issue CSRs for composite diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/AMQPBridgeManager.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/AMQPBridgeManager.kt index 523369b370..41f5a7d72b 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/AMQPBridgeManager.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/AMQPBridgeManager.kt @@ -12,6 +12,7 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER import net.corda.nodeapi.internal.ArtemisMessagingComponent.RemoteInboxAddress.Companion.translateLocalQueueToInboxAddress +import net.corda.nodeapi.internal.ArtemisSessionProvider import net.corda.nodeapi.internal.bridging.AMQPBridgeManager.AMQPBridge.Companion.getBridgeName import net.corda.nodeapi.internal.config.NodeSSLConfiguration import net.corda.nodeapi.internal.protonwrapper.messages.MessageStatus @@ -35,7 +36,7 @@ import kotlin.concurrent.withLock * The Netty thread pool used by the AMQPBridges is also shared and managed by the AMQPBridgeManager. */ @VisibleForTesting -class AMQPBridgeManager(val config: NodeSSLConfiguration, val p2pAddress: NetworkHostAndPort, val maxMessageSize: Int) : BridgeManager { +class AMQPBridgeManager(config: NodeSSLConfiguration, val artemisMessageClientFactory: () -> ArtemisSessionProvider) : BridgeManager { private val lock = ReentrantLock() private val bridgeNameToBridgeMap = mutableMapOf() @@ -43,7 +44,9 @@ class AMQPBridgeManager(val config: NodeSSLConfiguration, val p2pAddress: Networ private val keyStore = config.loadSslKeyStore().internal private val keyStorePrivateKeyPassword: String = config.keyStorePassword private val trustStore = config.loadTrustStore().internal - private var artemis: ArtemisMessagingClient? = null + private var artemis: ArtemisSessionProvider? = null + + constructor(config: NodeSSLConfiguration, p2pAddress: NetworkHostAndPort, maxMessageSize: Int) : this(config, { ArtemisMessagingClient(config, p2pAddress, maxMessageSize) }) companion object { private const val NUM_BRIDGE_THREADS = 0 // Default sized pool @@ -64,7 +67,7 @@ class AMQPBridgeManager(val config: NodeSSLConfiguration, val p2pAddress: Networ keyStorePrivateKeyPassword: String, trustStore: KeyStore, sharedEventGroup: EventLoopGroup, - private val artemis: ArtemisMessagingClient) { + private val artemis: ArtemisSessionProvider) { companion object { fun getBridgeName(queueName: String, hostAndPort: NetworkHostAndPort): String = "$queueName -> $hostAndPort" } @@ -155,9 +158,8 @@ class AMQPBridgeManager(val config: NodeSSLConfiguration, val p2pAddress: Networ } } - private fun gatherAddresses(node: NodeInfo): Sequence { - val address = node.addresses.single() - return node.legalIdentitiesAndCerts.map { ArtemisMessagingComponent.NodeAddress(it.party.owningKey, address) }.asSequence() + private fun gatherAddresses(node: NodeInfo): List { + return node.legalIdentitiesAndCerts.map { ArtemisMessagingComponent.NodeAddress(it.party.owningKey, node.addresses[0]) } } override fun deployBridge(queueName: String, target: NetworkHostAndPort, legalNames: Set) { @@ -191,7 +193,7 @@ class AMQPBridgeManager(val config: NodeSSLConfiguration, val p2pAddress: Networ override fun start() { sharedEventLoopGroup = NioEventLoopGroup(NUM_BRIDGE_THREADS) - val artemis = ArtemisMessagingClient(config, p2pAddress, maxMessageSize) + val artemis = artemisMessageClientFactory() this.artemis = artemis artemis.start() } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeControlListener.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeControlListener.kt index 570f28cf48..2d0d53a19d 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeControlListener.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeControlListener.kt @@ -10,6 +10,7 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.BRIDGE_CON import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.BRIDGE_NOTIFY import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX +import net.corda.nodeapi.internal.ArtemisSessionProvider import net.corda.nodeapi.internal.config.NodeSSLConfiguration import org.apache.activemq.artemis.api.core.RoutingType import org.apache.activemq.artemis.api.core.SimpleString @@ -18,14 +19,17 @@ import org.apache.activemq.artemis.api.core.client.ClientMessage import java.util.* class BridgeControlListener(val config: NodeSSLConfiguration, - val p2pAddress: NetworkHostAndPort, - val maxMessageSize: Int) : AutoCloseable { + val artemisMessageClientFactory: () -> ArtemisSessionProvider) : AutoCloseable { private val bridgeId: String = UUID.randomUUID().toString() - private val bridgeManager: BridgeManager = AMQPBridgeManager(config, p2pAddress, maxMessageSize) + private val bridgeManager: BridgeManager = AMQPBridgeManager(config, artemisMessageClientFactory) private val validInboundQueues = mutableSetOf() - private var artemis: ArtemisMessagingClient? = null + private var artemis: ArtemisSessionProvider? = null private var controlConsumer: ClientConsumer? = null + constructor(config: NodeSSLConfiguration, + p2pAddress: NetworkHostAndPort, + maxMessageSize: Int) : this(config, { ArtemisMessagingClient(config, p2pAddress, maxMessageSize) }) + companion object { private val log = contextLogger() } @@ -33,7 +37,7 @@ class BridgeControlListener(val config: NodeSSLConfiguration, fun start() { stop() bridgeManager.start() - val artemis = ArtemisMessagingClient(config, p2pAddress, maxMessageSize) + val artemis = artemisMessageClientFactory() this.artemis = artemis artemis.start() val artemisClient = artemis.started!! @@ -56,6 +60,7 @@ class BridgeControlListener(val config: NodeSSLConfiguration, } fun stop() { + validInboundQueues.clear() controlConsumer?.close() controlConsumer = null artemis?.stop() @@ -65,6 +70,10 @@ class BridgeControlListener(val config: NodeSSLConfiguration, override fun close() = stop() + fun validateReceiveTopic(topic: String): Boolean { + return topic in validInboundQueues + } + private fun validateInboxQueueName(queueName: String): Boolean { return queueName.startsWith(P2P_PREFIX) && artemis!!.started!!.session.queueQuery(SimpleString(queueName)).isExists } @@ -90,7 +99,6 @@ class BridgeControlListener(val config: NodeSSLConfiguration, for (outQueue in controlMessage.sendQueues) { bridgeManager.deployBridge(outQueue.queueName, outQueue.targets.first(), outQueue.legalNames.toSet()) } - // TODO For now we just record the inboxes, but we don't use the information, but eventually out of process bridges will use this for validating inbound messages. validInboundQueues.addAll(controlMessage.inboxQueues) } is BridgeControl.BridgeToNodeSnapshotRequest -> { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/ConfigUtilities.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/ConfigUtilities.kt index 94997d34b8..544f1c1f7e 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/ConfigUtilities.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/ConfigUtilities.kt @@ -2,10 +2,7 @@ package net.corda.nodeapi.internal.config -import com.typesafe.config.Config -import com.typesafe.config.ConfigFactory -import com.typesafe.config.ConfigUtil -import com.typesafe.config.ConfigValueFactory +import com.typesafe.config.* import net.corda.core.identity.CordaX500Name import net.corda.core.internal.noneOrSingle import net.corda.core.internal.uncheckedCast @@ -78,7 +75,12 @@ private fun Config.getSingleValue(path: String, type: KType): Any? { NetworkHostAndPort::class -> NetworkHostAndPort.parse(getString(path)) Path::class -> Paths.get(getString(path)) URL::class -> URL(getString(path)) - CordaX500Name::class -> CordaX500Name.parse(getString(path)) + CordaX500Name::class -> { + when (getValue(path).valueType()) { + ConfigValueType.OBJECT -> getConfig(path).parseAs() + else -> CordaX500Name.parse(getString(path)) + } + } Properties::class -> getConfig(path).toProperties() Config::class -> getConfig(path) else -> if (typeClass.java.isEnum) { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt index 5b103222a3..e7ff717bf2 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt @@ -259,13 +259,17 @@ object X509Utilities { private fun createCertificateSigningRequest(subject: X500Principal, email: String, keyPair: KeyPair, - signatureScheme: SignatureScheme): PKCS10CertificationRequest { + signatureScheme: SignatureScheme, + certRole: CertRole): PKCS10CertificationRequest { val signer = ContentSignerBuilder.build(signatureScheme, keyPair.private, Crypto.findProvider(signatureScheme.providerName)) - return JcaPKCS10CertificationRequestBuilder(subject, keyPair.public).addAttribute(BCStyle.E, DERUTF8String(email)).build(signer) + return JcaPKCS10CertificationRequestBuilder(subject, keyPair.public) + .addAttribute(BCStyle.E, DERUTF8String(email)) + .addAttribute(ASN1ObjectIdentifier(CordaOID.X509_EXTENSION_CORDA_ROLE), certRole) + .build(signer) } - fun createCertificateSigningRequest(subject: X500Principal, email: String, keyPair: KeyPair): PKCS10CertificationRequest { - return createCertificateSigningRequest(subject, email, keyPair, DEFAULT_TLS_SIGNATURE_SCHEME) + fun createCertificateSigningRequest(subject: X500Principal, email: String, keyPair: KeyPair, certRole: CertRole = CertRole.NODE_CA): PKCS10CertificationRequest { + return createCertificateSigningRequest(subject, email, keyPair, DEFAULT_TLS_SIGNATURE_SCHEME, certRole) } fun buildCertPath(first: X509Certificate, remaining: List): CertPath { @@ -284,19 +288,24 @@ object X509Utilities { } } +// Assuming cert type to role is 1:1 +val CertRole.certificateType: CertificateType get() = CertificateType.values().first { it.role == this } + /** * Convert a [X509Certificate] into Bouncycastle's [X509CertificateHolder]. * * NOTE: To avoid unnecessary copying use [X509Certificate] where possible. */ fun X509Certificate.toBc() = X509CertificateHolder(encoded) + fun X509CertificateHolder.toJca(): X509Certificate = X509CertificateFactory().generateCertificate(encoded.inputStream()) -val CertPath.x509Certificates: List get() { - require(type == "X.509") { "Not an X.509 cert path: $this" } - // We're not mapping the list to avoid creating a new one. - return uncheckedCast(certificates) -} +val CertPath.x509Certificates: List + get() { + require(type == "X.509") { "Not an X.509 cert path: $this" } + // We're not mapping the list to avoid creating a new one. + return uncheckedCast(certificates) + } val Certificate.x509: X509Certificate get() = requireNotNull(this as? X509Certificate) { "Not an X.509 certificate: $this" } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt index c8f90fcd74..cfd038a208 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt @@ -1,26 +1,33 @@ 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.nodeapi.internal.serialization.CordaSerializationMagic import net.corda.core.serialization.internal.SerializationEnvironmentImpl import net.corda.core.serialization.internal._contextSerializationEnv 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.CordaSerializationMagic 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.kryoMagic +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 +51,17 @@ class NetworkBootstrapper { ) private const val LOGS_DIR_NAME = "logs" + private const val WHITELIST_FILE_NAME = "whitelist.txt" @JvmStatic fun main(args: Array) { - 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?) { directory.createDirectories() println("Bootstrapping local network in $directory") generateDirectoriesIfNeeded(directory) @@ -69,7 +78,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?.distinct()) + println("Updating whitelist.") + overwriteWhitelist(directory / WHITELIST_FILE_NAME, mergedWhiteList) + installNetworkParameters(notaryInfos, nodeDirs, mergedWhiteList) println("Bootstrapping complete!") } finally { _contextSerializationEnv.set(null) @@ -85,8 +97,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) } @@ -150,7 +161,7 @@ class NetworkBootstrapper { if (nodeConfig.hasPath("notary")) { val validating = nodeConfig.getConfig("notary").getBoolean("validating") // And the node-info file contains the notary's identity - val nodeInfo = nodeInfoFile.readAll().deserialize().verified() + val nodeInfo = nodeInfoFile.readObject().verified() NotaryInfo(nodeInfo.notaryIdentity(), validating) } else { null @@ -158,7 +169,7 @@ class NetworkBootstrapper { }.distinct() // We need distinct as nodes part of a distributed notary share the same notary identity } - private fun installNetworkParameters(notaryInfos: List, nodeDirs: List) { + private fun installNetworkParameters(notaryInfos: List, nodeDirs: List, whitelist: Map>) { // TODO Add config for minimumPlatformVersion, maxMessageSize and maxTransactionSize val copier = NetworkParametersCopier(NetworkParameters( minimumPlatformVersion = 1, @@ -166,12 +177,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?): Map> { + 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>) { + 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> = 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 { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkMap.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkMap.kt index 52841d2946..31314ca833 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkMap.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkMap.kt @@ -14,6 +14,9 @@ import java.time.Instant const val NETWORK_PARAMS_FILE_NAME = "network-parameters" const val NETWORK_PARAMS_UPDATE_FILE_NAME = "network-parameters-update" +typealias SignedNetworkMap = SignedDataWithCert +typealias SignedNetworkParameters = SignedDataWithCert + /** * Data structure representing the network map available from the HTTP network map service as a serialised blob. * @property nodeInfoHashes list of network participant's [NodeInfo] hashes diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt index 9a2b2c3b64..4884c21832 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt @@ -43,7 +43,10 @@ enum class TransactionIsolationLevel { } private val _contextDatabase = ThreadLocal() -val contextDatabase get() = _contextDatabase.get() ?: error("Was expecting to find CordaPersistence set on current thread: ${Strand.currentStrand()}") +var contextDatabase: CordaPersistence + get() = _contextDatabase.get() ?: error("Was expecting to find CordaPersistence set on current thread: ${Strand.currentStrand()}") + set(database) = _contextDatabase.set(database) +val contextDatabaseOrNull: CordaPersistence? get() = _contextDatabase.get() class CordaPersistence( val dataSource: DataSource, diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/engine/ConnectionStateMachine.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/engine/ConnectionStateMachine.kt index 1ab83fcc4e..473c28876c 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/engine/ConnectionStateMachine.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/engine/ConnectionStateMachine.kt @@ -168,12 +168,14 @@ internal class ConnectionStateMachine(serverMode: Boolean, val transport = event.transport log.debug { "Transport Head Closed $transport" } transport.close_tail() + onTransportInternal(transport) } override fun onTransportTailClosed(event: Event) { val transport = event.transport log.debug { "Transport Tail Closed $transport" } transport.close_head() + onTransportInternal(transport) } override fun onTransportClosed(event: Event) { @@ -195,6 +197,7 @@ internal class ConnectionStateMachine(serverMode: Boolean, } else { log.info("Error (no description returned).") } + onTransportInternal(transport) } override fun onTransport(event: Event) { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/engine/EventProcessor.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/engine/EventProcessor.kt index 14c21b97f2..c02c6f2541 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/engine/EventProcessor.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/engine/EventProcessor.kt @@ -79,7 +79,10 @@ internal class EventProcessor(channel: Channel, if ((connection.localState != EndpointState.CLOSED) && !connection.transport.isClosed) { val now = System.currentTimeMillis() val tickDelay = Math.max(0L, connection.transport.tick(now) - now) - executor.schedule({ tick(connection) }, tickDelay, TimeUnit.MILLISECONDS) + executor.schedule({ + tick(connection) + processEvents() + }, tickDelay, TimeUnit.MILLISECONDS) } } catch (ex: Exception) { connection.transport.close() diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPChannelHandler.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPChannelHandler.kt index 4054b7c7fd..25855df558 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPChannelHandler.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPChannelHandler.kt @@ -38,8 +38,8 @@ internal class AMQPChannelHandler(private val serverMode: Boolean, private val onReceive: (ReceivedMessage) -> Unit) : ChannelDuplexHandler() { private val log = LoggerFactory.getLogger(allowedRemoteLegalNames?.firstOrNull()?.toString() ?: "AMQPChannelHandler") private lateinit var remoteAddress: InetSocketAddress - private lateinit var localCert: X509Certificate - private lateinit var remoteCert: X509Certificate + private var localCert: X509Certificate? = null + private var remoteCert: X509Certificate? = null private var eventProcessor: EventProcessor? = null override fun channelActive(ctx: ChannelHandlerContext) { @@ -51,7 +51,7 @@ internal class AMQPChannelHandler(private val serverMode: Boolean, private fun createAMQPEngine(ctx: ChannelHandlerContext) { val ch = ctx.channel() - eventProcessor = EventProcessor(ch, serverMode, localCert.subjectX500Principal.toString(), remoteCert.subjectX500Principal.toString(), userName, password) + eventProcessor = EventProcessor(ch, serverMode, localCert!!.subjectX500Principal.toString(), remoteCert!!.subjectX500Principal.toString(), userName, password) val connection = eventProcessor!!.connection val transport = connection.transport as ProtonJTransport if (trace) { @@ -72,7 +72,7 @@ internal class AMQPChannelHandler(private val serverMode: Boolean, override fun channelInactive(ctx: ChannelHandlerContext) { val ch = ctx.channel() log.info("Closed client connection ${ch.id()} from $remoteAddress to ${ch.localAddress()}") - onClose(Pair(ch as SocketChannel, ConnectionChange(remoteAddress, null, false))) + onClose(Pair(ch as SocketChannel, ConnectionChange(remoteAddress, remoteCert, false))) eventProcessor?.close() ctx.fireChannelInactive() } @@ -84,7 +84,7 @@ internal class AMQPChannelHandler(private val serverMode: Boolean, localCert = sslHandler.engine().session.localCertificates[0].x509 remoteCert = sslHandler.engine().session.peerCertificates[0].x509 try { - val remoteX500Name = CordaX500Name.build(remoteCert.subjectX500Principal) + val remoteX500Name = CordaX500Name.build(remoteCert!!.subjectX500Principal) require(allowedRemoteLegalNames == null || remoteX500Name in allowedRemoteLegalNames) log.info("handshake completed subject: $remoteX500Name") } catch (ex: IllegalArgumentException) { @@ -124,7 +124,7 @@ internal class AMQPChannelHandler(private val serverMode: Boolean, require(inetAddress == remoteAddress) { "Message for incorrect endpoint" } - require(CordaX500Name.parse(msg.destinationLegalName) == CordaX500Name.build(remoteCert.subjectX500Principal)) { + require(CordaX500Name.parse(msg.destinationLegalName) == CordaX500Name.build(remoteCert!!.subjectX500Principal)) { "Message for incorrect legal identity" } log.debug { "channel write ${msg.applicationProperties["_AMQ_DUPL_ID"]}" } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPServer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPServer.kt index 588c7fb8a0..9c98767d0e 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPServer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPServer.kt @@ -38,7 +38,7 @@ class AMQPServer(val hostName: String, private val userName: String?, private val password: String?, private val keyStore: KeyStore, - private val keyStorePrivateKeyPassword: String, + private val keyStorePrivateKeyPassword: CharArray, private val trustStore: KeyStore, private val trace: Boolean = false) : AutoCloseable { @@ -59,15 +59,21 @@ class AMQPServer(val hostName: String, private var serverChannel: Channel? = null private val clientChannels = ConcurrentHashMap() - init { - } + constructor(hostName: String, + port: Int, + userName: String?, + password: String?, + keyStore: KeyStore, + keyStorePrivateKeyPassword: String, + trustStore: KeyStore, + trace: Boolean = false) : this(hostName, port, userName, password, keyStore, keyStorePrivateKeyPassword.toCharArray(), trustStore, trace) private class ServerChannelInitializer(val parent: AMQPServer) : ChannelInitializer() { private val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) private val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) init { - keyManagerFactory.init(parent.keyStore, parent.keyStorePrivateKeyPassword.toCharArray()) + keyManagerFactory.init(parent.keyStore, parent.keyStorePrivateKeyPassword) trustManagerFactory.init(parent.trustStore) } @@ -169,6 +175,13 @@ class AMQPServer(val hostName: String, } } + fun dropConnection(connectionRemoteHost: InetSocketAddress) { + val channel = clientChannels[connectionRemoteHost] + if (channel != null) { + channel.close() + } + } + fun complete(delivery: Delivery, target: InetSocketAddress) { val channel = clientChannels[target] channel?.apply { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/ClientContexts.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/ClientContexts.kt index 3c2eb8fa76..e4e2f53417 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/ClientContexts.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/ClientContexts.kt @@ -18,10 +18,12 @@ val KRYO_RPC_CLIENT_CONTEXT = SerializationContextImpl(kryoMagic, GlobalTransientClassWhiteList(BuiltInExceptionsWhitelist()), emptyMap(), true, - SerializationContext.UseCase.RPCClient) + SerializationContext.UseCase.RPCClient, + null) val AMQP_RPC_CLIENT_CONTEXT = SerializationContextImpl(amqpMagic, SerializationDefaults.javaClass.classLoader, GlobalTransientClassWhiteList(BuiltInExceptionsWhitelist()), emptyMap(), true, - SerializationContext.UseCase.RPCClient) + SerializationContext.UseCase.RPCClient, + null) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/OrdinalIO.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/OrdinalIO.kt new file mode 100644 index 0000000000..6e04d490f4 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/OrdinalIO.kt @@ -0,0 +1,31 @@ +package net.corda.nodeapi.internal.serialization + +import java.io.EOFException +import java.io.InputStream +import java.io.OutputStream +import java.nio.ByteBuffer + +class OrdinalBits(private val ordinal: Int) { + interface OrdinalWriter { + val bits: OrdinalBits + val encodedSize get() = 1 + fun writeTo(stream: OutputStream) = stream.write(bits.ordinal) + fun putTo(buffer: ByteBuffer) = buffer.put(bits.ordinal.toByte())!! + } + + init { + require(ordinal >= 0) { "The ordinal must be non-negative." } + require(ordinal < 128) { "Consider implementing a varint encoding." } + } +} + +class OrdinalReader(private val values: Array) { + private val enumName = values[0].javaClass.simpleName + private val range = 0 until values.size + fun readFrom(stream: InputStream): E { + val ordinal = stream.read() + if (ordinal == -1) throw EOFException("Expected a $enumName ordinal.") + if (ordinal !in range) throw NoSuchElementException("No $enumName with ordinal: $ordinal") + return values[ordinal] + } +} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationFormat.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationFormat.kt index 6414efbb17..fefdfb930f 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationFormat.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationFormat.kt @@ -1,8 +1,17 @@ package net.corda.nodeapi.internal.serialization +import net.corda.core.internal.VisibleForTesting +import net.corda.core.serialization.SerializationEncoding import net.corda.core.utilities.ByteSequence import net.corda.core.utilities.OpaqueBytes +import net.corda.nodeapi.internal.serialization.OrdinalBits.OrdinalWriter +import org.iq80.snappy.SnappyFramedInputStream +import org.iq80.snappy.SnappyFramedOutputStream +import java.io.OutputStream +import java.io.InputStream import java.nio.ByteBuffer +import java.util.zip.DeflaterOutputStream +import java.util.zip.InflaterInputStream class CordaSerializationMagic(bytes: ByteArray) : OpaqueBytes(bytes) { private val bufferView = slice() @@ -10,3 +19,40 @@ class CordaSerializationMagic(bytes: ByteArray) : OpaqueBytes(bytes) { return if (data.slice(end = size) == bufferView) data.slice(size) else null } } + +enum class SectionId : OrdinalWriter { + /** Serialization data follows, and then discard the rest of the stream (if any) as legacy data may have trailing garbage. */ + DATA_AND_STOP, + /** Identical behaviour to [DATA_AND_STOP], historically used for Kryo. Do not use in new code. */ + ALT_DATA_AND_STOP, + /** The ordinal of a [CordaSerializationEncoding] follows, which should be used to decode the remainder of the stream. */ + ENCODING; + + companion object { + val reader = OrdinalReader(values()) + } + + override val bits = OrdinalBits(ordinal) +} + +enum class CordaSerializationEncoding : SerializationEncoding, OrdinalWriter { + DEFLATE { + override fun wrap(stream: OutputStream) = DeflaterOutputStream(stream) + override fun wrap(stream: InputStream) = InflaterInputStream(stream) + }, + SNAPPY { + override fun wrap(stream: OutputStream) = SnappyFramedOutputStream(stream) + override fun wrap(stream: InputStream) = SnappyFramedInputStream(stream, false) + }; + + companion object { + val reader = OrdinalReader(values()) + } + + override val bits = OrdinalBits(ordinal) + abstract fun wrap(stream: OutputStream): OutputStream + abstract fun wrap(stream: InputStream): InputStream +} + +@VisibleForTesting +internal val encodingNotPermittedFormat = "Encoding not permitted: %s" diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt index 95dcc9b603..a093bc871b 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt @@ -18,13 +18,18 @@ import java.util.concurrent.ExecutionException val attachmentsClassLoaderEnabledPropertyName = "attachments.class.loader.enabled" -data class SerializationContextImpl(override val preferredSerializationVersion: SerializationMagic, - override val deserializationClassLoader: ClassLoader, - override val whitelist: ClassWhitelist, - override val properties: Map, - override val objectReferencesEnabled: Boolean, - override val useCase: SerializationContext.UseCase) : SerializationContext { +internal object NullEncodingWhitelist : EncodingWhitelist { + override fun acceptEncoding(encoding: SerializationEncoding) = false +} +data class SerializationContextImpl @JvmOverloads constructor(override val preferredSerializationVersion: SerializationMagic, + override val deserializationClassLoader: ClassLoader, + override val whitelist: ClassWhitelist, + override val properties: Map, + override val objectReferencesEnabled: Boolean, + override val useCase: SerializationContext.UseCase, + override val encoding: SerializationEncoding?, + override val encodingWhitelist: EncodingWhitelist = NullEncodingWhitelist) : SerializationContext { private val cache: Cache, AttachmentsClassLoader> = CacheBuilder.newBuilder().weakValues().maximumSize(1024).build() /** @@ -70,6 +75,7 @@ data class SerializationContextImpl(override val preferredSerializationVersion: } override fun withPreferredSerializationVersion(magic: SerializationMagic) = copy(preferredSerializationVersion = magic) + override fun withEncoding(encoding: SerializationEncoding?) = copy(encoding = encoding) } open class SerializationFactoryImpl : SerializationFactory() { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/ServerContexts.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/ServerContexts.kt index a2bfa64628..cc8dcfa305 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/ServerContexts.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/ServerContexts.kt @@ -27,22 +27,26 @@ val KRYO_RPC_SERVER_CONTEXT = SerializationContextImpl(kryoMagic, GlobalTransientClassWhiteList(BuiltInExceptionsWhitelist()), emptyMap(), true, - SerializationContext.UseCase.RPCServer) + SerializationContext.UseCase.RPCServer, + null) val KRYO_STORAGE_CONTEXT = SerializationContextImpl(kryoMagic, SerializationDefaults.javaClass.classLoader, AllButBlacklisted, emptyMap(), true, - SerializationContext.UseCase.Storage) + SerializationContext.UseCase.Storage, + null) val AMQP_STORAGE_CONTEXT = SerializationContextImpl(amqpMagic, SerializationDefaults.javaClass.classLoader, AllButBlacklisted, emptyMap(), true, - SerializationContext.UseCase.Storage) + SerializationContext.UseCase.Storage, + null) val AMQP_RPC_SERVER_CONTEXT = SerializationContextImpl(amqpMagic, SerializationDefaults.javaClass.classLoader, GlobalTransientClassWhiteList(BuiltInExceptionsWhitelist()), emptyMap(), true, - SerializationContext.UseCase.RPCServer) + SerializationContext.UseCase.RPCServer, + null) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SharedContexts.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SharedContexts.kt index 25e4e278a1..9620b3c999 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SharedContexts.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SharedContexts.kt @@ -20,18 +20,19 @@ val KRYO_P2P_CONTEXT = SerializationContextImpl(kryoMagic, GlobalTransientClassWhiteList(BuiltInExceptionsWhitelist()), emptyMap(), true, - SerializationContext.UseCase.P2P) + SerializationContext.UseCase.P2P, + null) val KRYO_CHECKPOINT_CONTEXT = SerializationContextImpl(kryoMagic, SerializationDefaults.javaClass.classLoader, QuasarWhitelist, emptyMap(), true, - SerializationContext.UseCase.Checkpoint) + SerializationContext.UseCase.Checkpoint, + null) val AMQP_P2P_CONTEXT = SerializationContextImpl(amqpMagic, SerializationDefaults.javaClass.classLoader, GlobalTransientClassWhiteList(BuiltInExceptionsWhitelist()), emptyMap(), true, - SerializationContext.UseCase.P2P) - - + SerializationContext.UseCase.P2P, + null) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPStreams.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPStreams.kt new file mode 100644 index 0000000000..f45ac6d864 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPStreams.kt @@ -0,0 +1,31 @@ +package net.corda.nodeapi.internal.serialization.amqp + +import com.esotericsoftware.kryo.io.ByteBufferInputStream +import net.corda.nodeapi.internal.serialization.kryo.ByteBufferOutputStream +import net.corda.nodeapi.internal.serialization.kryo.serializeOutputStreamPool +import java.io.InputStream +import java.io.OutputStream +import java.nio.ByteBuffer + +fun InputStream.asByteBuffer(): ByteBuffer { + return if (this is ByteBufferInputStream) { + byteBuffer // BBIS has no other state, so this is perfectly safe. + } else { + ByteBuffer.wrap(serializeOutputStreamPool.run { + copyTo(it) + it.toByteArray() + }) + } +} + +fun OutputStream.alsoAsByteBuffer(remaining: Int, task: (ByteBuffer) -> T): T { + return if (this is ByteBufferOutputStream) { + alsoAsByteBuffer(remaining, task) + } else { + serializeOutputStreamPool.run { + val result = it.alsoAsByteBuffer(remaining, task) + it.copyTo(this) + result + } + } +} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt index da555798e1..cdde047ef8 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt @@ -1,18 +1,27 @@ package net.corda.nodeapi.internal.serialization.amqp +import com.esotericsoftware.kryo.io.ByteBufferInputStream +import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.getStackTraceAsString +import net.corda.core.serialization.EncodingWhitelist import net.corda.core.serialization.SerializedBytes import net.corda.core.utilities.ByteSequence +import net.corda.nodeapi.internal.serialization.CordaSerializationEncoding +import net.corda.nodeapi.internal.serialization.NullEncodingWhitelist +import net.corda.nodeapi.internal.serialization.SectionId +import net.corda.nodeapi.internal.serialization.encodingNotPermittedFormat import org.apache.qpid.proton.amqp.Binary import org.apache.qpid.proton.amqp.DescribedType import org.apache.qpid.proton.amqp.UnsignedByte import org.apache.qpid.proton.amqp.UnsignedInteger import org.apache.qpid.proton.codec.Data +import java.io.InputStream import java.io.NotSerializableException import java.lang.reflect.ParameterizedType import java.lang.reflect.Type import java.lang.reflect.TypeVariable import java.lang.reflect.WildcardType +import java.nio.ByteBuffer data class ObjectAndEnvelope(val obj: T, val envelope: Envelope) @@ -22,7 +31,8 @@ data class ObjectAndEnvelope(val obj: T, val envelope: Envelope) * @param serializerFactory This is the factory for [AMQPSerializer] instances and can be shared across multiple * instances and threads. */ -class DeserializationInput(internal val serializerFactory: SerializerFactory) { +class DeserializationInput @JvmOverloads constructor(private val serializerFactory: SerializerFactory, + private val encodingWhitelist: EncodingWhitelist = NullEncodingWhitelist) { private val objectHistory: MutableList = mutableListOf() internal companion object { @@ -47,6 +57,28 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) { } return size + BYTES_NEEDED_TO_PEEK } + + @VisibleForTesting + @Throws(NotSerializableException::class) + internal fun withDataBytes(byteSequence: ByteSequence, encodingWhitelist: EncodingWhitelist, task: (ByteBuffer) -> T): T { + // Check that the lead bytes match expected header + val amqpSequence = amqpMagic.consume(byteSequence) ?: throw NotSerializableException("Serialization header does not match.") + var stream: InputStream = ByteBufferInputStream(amqpSequence) + try { + while (true) { + when (SectionId.reader.readFrom(stream)) { + SectionId.ENCODING -> { + val encoding = CordaSerializationEncoding.reader.readFrom(stream) + encodingWhitelist.acceptEncoding(encoding) || throw NotSerializableException(encodingNotPermittedFormat.format(encoding)) + stream = encoding.wrap(stream) + } + SectionId.DATA_AND_STOP, SectionId.ALT_DATA_AND_STOP -> return task(stream.asByteBuffer()) + } + } + } finally { + stream.close() + } + } } @Throws(NotSerializableException::class) @@ -58,12 +90,12 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) { @Throws(NotSerializableException::class) internal fun getEnvelope(byteSequence: ByteSequence): Envelope { - // Check that the lead bytes match expected header - val dataBytes = amqpMagic.consume(byteSequence) ?: throw NotSerializableException("Serialization header does not match.") - val data = Data.Factory.create() - val expectedSize = dataBytes.remaining() - if (data.decode(dataBytes) != expectedSize.toLong()) throw NotSerializableException("Unexpected size of data") - return Envelope.get(data) + return withDataBytes(byteSequence, encodingWhitelist) { dataBytes -> + val data = Data.Factory.create() + val expectedSize = dataBytes.remaining() + if (data.decode(dataBytes) != expectedSize.toLong()) throw NotSerializableException("Unexpected size of data") + Envelope.get(data) + } } @Throws(NotSerializableException::class) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt index 285a1f5d54..1318d066fe 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt @@ -12,7 +12,7 @@ import net.corda.nodeapi.internal.serialization.carpenter.Field as CarpenterFiel import net.corda.nodeapi.internal.serialization.carpenter.Schema as CarpenterSchema const val DESCRIPTOR_DOMAIN: String = "net.corda" -val amqpMagic = CordaSerializationMagic("corda".toByteArray() + byteArrayOf(1, 0, 0)) +val amqpMagic = CordaSerializationMagic("corda".toByteArray() + byteArrayOf(1, 0)) /** * This and the classes below are OO representations of the AMQP XML schema described in the specification. Their diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt index 4ce172cc78..83e3486d5a 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt @@ -213,8 +213,13 @@ internal fun propertiesForSerializationFromConstructor( return mutableListOf().apply { kotlinConstructor.parameters.withIndex().forEach { param -> - val name = param.value.name ?: throw NotSerializableException( - "Constructor parameter of $clazz has no name.") + // If a parameter doesn't have a name *at all* then chances are it's a synthesised + // one. A good example of this is non static nested classes in Java where instances + // of the nested class require access to the outer class without breaking + // encapsulation. Thus a parameter is inserted into the constructor that passes a + // reference to the enclosing class. In this case we can't do anything with + // it so just ignore it as it'll be supplied at runtime anyway on invocation + val name = param.value.name ?: return@forEach val propertyReader = if (name in classProperties) { if (classProperties[name]!!.getter != null) { @@ -223,7 +228,7 @@ internal fun propertiesForSerializationFromConstructor( // Check that the method has a getter in java. val getter = matchingProperty.getter ?: throw NotSerializableException( - "Property has no getter method for $name of $clazz. If using Java and the parameter name" + "Property has no getter method for - \"$name\" - of \"$clazz\". If using Java and the parameter name" + "looks anonymous, check that you have the -parameters option specified in the " + "Java compiler. Alternately, provide a proxy serializer " + "(SerializationCustomSerializer) if recompiling isn't an option.") @@ -231,15 +236,15 @@ internal fun propertiesForSerializationFromConstructor( val returnType = resolveTypeVariables(getter.genericReturnType, type) if (!constructorParamTakesReturnTypeOfGetter(returnType, getter.genericReturnType, param.value)) { throw NotSerializableException( - "Property '$name' has type '$returnType' on class '$clazz' but differs from constructor " + - "parameter type '${param.value.type.javaType}'") + "Property - \"$name\" - has type \"$returnType\" on \"$clazz\" but differs from constructor " + + "parameter type \"${param.value.type.javaType}\"") } Pair(PublicPropertyReader(getter), returnType) } else { val field = classProperties[name]!!.field ?: - throw NotSerializableException("No property matching constructor parameter named '$name' " + - "of '$clazz'. If using Java, check that you have the -parameters option specified " + + throw NotSerializableException("No property matching constructor parameter named - \"$name\" - " + + "of \"$clazz\". If using Java, check that you have the -parameters option specified " + "in the Java compiler. Alternately, provide a proxy serializer " + "(SerializationCustomSerializer) if recompiling isn't an option") @@ -247,7 +252,7 @@ internal fun propertiesForSerializationFromConstructor( } } else { throw NotSerializableException( - "Constructor parameter $name doesn't refer to a property of class '$clazz'") + "Constructor parameter - \"$name\" - doesn't refer to a property of \"$clazz\"") } this += PropertyAccessorConstructor( diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutput.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutput.kt index f6b9972ec7..1dcf750ef5 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutput.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutput.kt @@ -1,10 +1,14 @@ package net.corda.nodeapi.internal.serialization.amqp +import net.corda.core.serialization.SerializationEncoding import net.corda.core.serialization.SerializedBytes +import net.corda.nodeapi.internal.serialization.CordaSerializationEncoding +import net.corda.nodeapi.internal.serialization.SectionId +import net.corda.nodeapi.internal.serialization.kryo.byteArrayOutput import org.apache.qpid.proton.codec.Data import java.io.NotSerializableException +import java.io.OutputStream import java.lang.reflect.Type -import java.nio.ByteBuffer import java.util.* import kotlin.collections.LinkedHashSet @@ -19,8 +23,7 @@ data class BytesAndSchemas( * @param serializerFactory This is the factory for [AMQPSerializer] instances and can be shared across multiple * instances and threads. */ -open class SerializationOutput(internal val serializerFactory: SerializerFactory) { - +open class SerializationOutput @JvmOverloads constructor(internal val serializerFactory: SerializerFactory, private val encoding: SerializationEncoding? = null) { private val objectHistory: MutableMap = IdentityHashMap() private val serializerHistory: MutableSet> = LinkedHashSet() internal val schemaHistory: MutableSet = LinkedHashSet() @@ -67,11 +70,21 @@ open class SerializationOutput(internal val serializerFactory: SerializerFactory writeTransformSchema(TransformsSchema.build(schema, serializerFactory), this) } } - val bytes = ByteArray(data.encodedSize().toInt() + 8) - val buf = ByteBuffer.wrap(bytes) - amqpMagic.putTo(buf) - data.encode(buf) - return SerializedBytes(bytes) + return SerializedBytes(byteArrayOutput { + var stream: OutputStream = it + try { + amqpMagic.writeTo(stream) + if (encoding != null) { + SectionId.ENCODING.writeTo(stream) + (encoding as CordaSerializationEncoding).writeTo(stream) + stream = encoding.wrap(stream) + } + SectionId.DATA_AND_STOP.writeTo(stream) + stream.alsoAsByteBuffer(data.encodedSize().toInt(), data::encode) + } finally { + stream.close() + } + }) } internal fun writeObject(obj: Any, data: Data) { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/ContractAttachmentSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/ContractAttachmentSerializer.kt index 2440114a4c..917057f428 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/ContractAttachmentSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/ContractAttachmentSerializer.kt @@ -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, val uploader: String?) } \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/DefaultKryoCustomizer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/DefaultKryoCustomizer.kt index a8d2e48649..63a08fd3c4 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/DefaultKryoCustomizer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/DefaultKryoCustomizer.kt @@ -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 @@ -206,29 +207,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 { if (kryo.serializationContext() != null) { val attachmentHash = SecureHash.SHA256(input.readBytes(32)) val contract = input.readString() - + val additionalContracts = kryo.readClassAndObject(input) as Set + 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 + val uploader = input.readString() + return ContractAttachment(attachment, contract, additionalContracts, uploader) } } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/KryoSerializationScheme.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/KryoSerializationScheme.kt index 719d982db3..7e1b94fffd 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/KryoSerializationScheme.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/KryoSerializationScheme.kt @@ -16,10 +16,12 @@ import net.corda.core.utilities.ByteSequence import net.corda.core.serialization.* import net.corda.nodeapi.internal.serialization.CordaSerializationMagic import net.corda.nodeapi.internal.serialization.CordaClassResolver +import net.corda.nodeapi.internal.serialization.SectionId import net.corda.nodeapi.internal.serialization.SerializationScheme +import net.corda.nodeapi.internal.serialization.* import java.security.PublicKey -val kryoMagic = CordaSerializationMagic("corda".toByteArray() + byteArrayOf(0, 0, 1)) +val kryoMagic = CordaSerializationMagic("corda".toByteArray() + byteArrayOf(0, 0)) private object AutoCloseableSerialisationDetector : Serializer() { override fun write(kryo: Kryo, output: Output, closeable: AutoCloseable) { @@ -87,11 +89,25 @@ abstract class AbstractKryoSerializationScheme : SerializationScheme { val dataBytes = kryoMagic.consume(byteSequence) ?: throw KryoException("Serialized bytes header does not match expected format.") return context.kryo { kryoInput(ByteBufferInputStream(dataBytes)) { - if (context.objectReferencesEnabled) { - uncheckedCast(readClassAndObject(this)) - } else { - withoutReferences { uncheckedCast(readClassAndObject(this)) } + val result: T + loop@ while (true) { + when (SectionId.reader.readFrom(this)) { + SectionId.ENCODING -> { + val encoding = CordaSerializationEncoding.reader.readFrom(this) + context.encodingWhitelist.acceptEncoding(encoding) || throw KryoException(encodingNotPermittedFormat.format(encoding)) + substitute(encoding::wrap) + } + SectionId.DATA_AND_STOP, SectionId.ALT_DATA_AND_STOP -> { + result = if (context.objectReferencesEnabled) { + uncheckedCast(readClassAndObject(this)) + } else { + withoutReferences { uncheckedCast(readClassAndObject(this)) } + } + break@loop + } + } } + result } } } @@ -100,6 +116,12 @@ abstract class AbstractKryoSerializationScheme : SerializationScheme { return context.kryo { SerializedBytes(kryoOutput { kryoMagic.writeTo(this) + context.encoding?.let { encoding -> + SectionId.ENCODING.writeTo(this) + (encoding as CordaSerializationEncoding).writeTo(this) + substitute(encoding::wrap) + } + SectionId.ALT_DATA_AND_STOP.writeTo(this) // Forward-compatible in null-encoding case. if (context.objectReferencesEnabled) { writeClassAndObject(this, obj) } else { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/KryoStreams.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/KryoStreams.kt index 9a34131a30..b1274223cc 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/KryoStreams.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/KryoStreams.kt @@ -4,13 +4,34 @@ import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output import net.corda.core.internal.LazyPool import java.io.* +import java.nio.ByteBuffer + +class ByteBufferOutputStream(size: Int) : ByteArrayOutputStream(size) { + companion object { + private val ensureCapacity = ByteArrayOutputStream::class.java.getDeclaredMethod("ensureCapacity", Int::class.java).apply { + isAccessible = true + } + } + + fun alsoAsByteBuffer(remaining: Int, task: (ByteBuffer) -> T): T { + ensureCapacity.invoke(this, count + remaining) + val buffer = ByteBuffer.wrap(buf, count, remaining) + val result = task(buffer) + count = buffer.position() + return result + } + + fun copyTo(stream: OutputStream) { + stream.write(buf, 0, count) + } +} private val serializationBufferPool = LazyPool( newInstance = { ByteArray(64 * 1024) }) -private val serializeOutputStreamPool = LazyPool( - clear = ByteArrayOutputStream::reset, +internal val serializeOutputStreamPool = LazyPool( + clear = ByteBufferOutputStream::reset, shouldReturnToPool = { it.size() < 256 * 1024 }, // Discard if it grew too large - newInstance = { ByteArrayOutputStream(64 * 1024) }) + newInstance = { ByteBufferOutputStream(64 * 1024) }) internal fun kryoInput(underlying: InputStream, task: Input.() -> T): T { return serializationBufferPool.run { @@ -22,13 +43,19 @@ internal fun kryoInput(underlying: InputStream, task: Input.() -> T): T { } internal fun kryoOutput(task: Output.() -> T): ByteArray { - return serializeOutputStreamPool.run { underlying -> + return byteArrayOutput { underlying -> serializationBufferPool.run { Output(it).use { output -> output.outputStream = underlying output.task() } } + } +} + +internal fun byteArrayOutput(task: (ByteBufferOutputStream) -> T): ByteArray { + return serializeOutputStreamPool.run { underlying -> + task(underlying) underlying.toByteArray() // Must happen after close, to allow ZIP footer to be written for example. } } diff --git a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/ForbiddenLambdaSerializationTests.java b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/ForbiddenLambdaSerializationTests.java index 265cbf098b..123bf60e0f 100644 --- a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/ForbiddenLambdaSerializationTests.java +++ b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/ForbiddenLambdaSerializationTests.java @@ -33,7 +33,7 @@ public final class ForbiddenLambdaSerializationTests { EnumSet contexts = EnumSet.complementOf(EnumSet.of(SerializationContext.UseCase.Checkpoint)); contexts.forEach(ctx -> { - SerializationContext context = new SerializationContextImpl(KryoSerializationSchemeKt.getKryoMagic(), this.getClass().getClassLoader(), AllWhitelist.INSTANCE, Maps.newHashMap(), true, ctx); + SerializationContext context = new SerializationContextImpl(KryoSerializationSchemeKt.getKryoMagic(), this.getClass().getClassLoader(), AllWhitelist.INSTANCE, Maps.newHashMap(), true, ctx, null); String value = "Hey"; Callable target = (Callable & Serializable) () -> value; @@ -55,7 +55,7 @@ public final class ForbiddenLambdaSerializationTests { EnumSet contexts = EnumSet.complementOf(EnumSet.of(SerializationContext.UseCase.Checkpoint)); contexts.forEach(ctx -> { - SerializationContext context = new SerializationContextImpl(KryoSerializationSchemeKt.getKryoMagic(), this.getClass().getClassLoader(), AllWhitelist.INSTANCE, Maps.newHashMap(), true, ctx); + SerializationContext context = new SerializationContextImpl(KryoSerializationSchemeKt.getKryoMagic(), this.getClass().getClassLoader(), AllWhitelist.INSTANCE, Maps.newHashMap(), true, ctx, null); String value = "Hey"; Callable target = () -> value; diff --git a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/LambdaCheckpointSerializationTest.java b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/LambdaCheckpointSerializationTest.java index 0203c498f4..6482240ba9 100644 --- a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/LambdaCheckpointSerializationTest.java +++ b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/LambdaCheckpointSerializationTest.java @@ -26,7 +26,7 @@ public final class LambdaCheckpointSerializationTest { @Before public void setup() { factory = testSerialization.getSerializationFactory(); - context = new SerializationContextImpl(KryoSerializationSchemeKt.getKryoMagic(), this.getClass().getClassLoader(), AllWhitelist.INSTANCE, Maps.newHashMap(), true, SerializationContext.UseCase.Checkpoint); + context = new SerializationContextImpl(KryoSerializationSchemeKt.getKryoMagic(), this.getClass().getClassLoader(), AllWhitelist.INSTANCE, Maps.newHashMap(), true, SerializationContext.UseCase.Checkpoint, null); } @Test diff --git a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java index 0441b2720f..c51ae5aff9 100644 --- a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java +++ b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java @@ -1,5 +1,8 @@ package net.corda.nodeapi.internal.serialization.amqp; +import com.google.common.collect.ImmutableList; +import net.corda.core.contracts.ContractState; +import net.corda.core.identity.AbstractParty; import net.corda.nodeapi.internal.serialization.AllWhitelist; import net.corda.core.serialization.SerializedBytes; import org.apache.qpid.proton.codec.DecoderImpl; @@ -9,6 +12,7 @@ import org.junit.Test; import javax.annotation.Nonnull; import java.io.NotSerializableException; import java.nio.ByteBuffer; +import java.util.List; import java.util.Objects; import static org.junit.Assert.assertTrue; @@ -237,4 +241,24 @@ public class JavaSerializationOutputTests { BoxedFooNotNull obj = new BoxedFooNotNull("Hello World!", 123); serdes(obj); } + + protected class DummyState implements ContractState { + @Override + public List getParticipants() { + return ImmutableList.of(); + } + } + + @Test + public void dummyStateSerialize() throws NotSerializableException { + SerializerFactory factory1 = new SerializerFactory( + AllWhitelist.INSTANCE, + ClassLoader.getSystemClassLoader(), + new EvolutionSerializerGetter(), + new SerializerFingerPrinter()); + + SerializationOutput serializer = new SerializationOutput(factory1); + + serializer.serialize(new DummyState()); + } } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt index 519c1632fc..19c07d1efd 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt @@ -13,10 +13,12 @@ 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 import net.corda.testing.internal.rigorousMock +import net.corda.testing.internal.MockCordappConfigProvider import net.corda.testing.services.MockAttachmentStorage import org.junit.Assert.* import org.junit.Rule @@ -58,7 +60,8 @@ class AttachmentsClassLoaderStaticContractTests { } private val serviceHub = rigorousMock().also { - doReturn(CordappProviderImpl(CordappLoader.createWithTestPackages(listOf("net.corda.nodeapi.internal")), MockAttachmentStorage())).whenever(it).cordappProvider + doReturn(CordappProviderImpl(CordappLoader.createWithTestPackages(listOf("net.corda.nodeapi.internal")), MockCordappConfigProvider(), MockAttachmentStorage(), testNetworkParameters().whitelistedContractImplementations)).whenever(it).cordappProvider + doReturn(testNetworkParameters()).whenever(it).networkParameters } @Test diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderTests.kt index af8e806874..eeafa28630 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderTests.kt @@ -18,11 +18,13 @@ 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 import net.corda.testing.internal.kryoSpecific import net.corda.testing.internal.rigorousMock +import net.corda.testing.internal.MockCordappConfigProvider import net.corda.testing.services.MockAttachmentStorage import org.apache.commons.io.IOUtils import org.junit.Assert.* @@ -57,12 +59,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)), MockCordappConfigProvider(), 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().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 +283,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 +307,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 diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/config/ConfigParsingTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/config/ConfigParsingTest.kt index a6fa2ca169..7fbc481321 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/config/ConfigParsingTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/config/ConfigParsingTest.kt @@ -87,10 +87,15 @@ class ConfigParsingTest { @Test fun CordaX500Name() { + val name1 = CordaX500Name(organisation = "Mock Party", locality = "London", country = "GB") testPropertyType( - CordaX500Name(organisation = "Mock Party", locality = "London", country = "GB"), + name1, CordaX500Name(organisation = "Mock Party 2", locality = "London", country = "GB"), valuesToString = true) + + // Test with config object. + val config = config("value" to mapOf("organisation" to "Mock Party", "locality" to "London", "country" to "GB")) + assertThat(config.parseAs().value).isEqualTo(name1) } @Test @@ -273,6 +278,7 @@ class ConfigParsingTest { data class OldData( @OldConfig("oldValue") val newValue: String) + data class DataWithCompanion(val value: Int) { companion object { @Suppress("unused") diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt index 2f3e7420fd..8e5aa1c776 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt @@ -319,7 +319,8 @@ class X509UtilitiesTest { AllWhitelist, emptyMap(), true, - SerializationContext.UseCase.P2P) + SerializationContext.UseCase.P2P, + null) val expected = X509Utilities.createSelfSignedCACertificate(ALICE.name.x500Principal, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) val serialized = expected.serialize(factory, context).bytes val actual = serialized.deserialize(factory, context) @@ -334,7 +335,8 @@ class X509UtilitiesTest { AllWhitelist, emptyMap(), true, - SerializationContext.UseCase.P2P) + SerializationContext.UseCase.P2P, + null) val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val rootCACert = X509Utilities.createSelfSignedCACertificate(ALICE_NAME.x500Principal, rootCAKey) val certificate = X509Utilities.createCertificate(CertificateType.TLS, rootCACert, rootCAKey, BOB_NAME.x500Principal, BOB.publicKey) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ContractAttachmentSerializerTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ContractAttachmentSerializerTest.kt index 01e296324d..3745da55d8 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ContractAttachmentSerializerTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ContractAttachmentSerializerTest.kt @@ -43,6 +43,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()) } @@ -58,6 +59,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()) } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolverTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolverTests.kt index a66f3038b4..90523d6f0e 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolverTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolverTests.kt @@ -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 @@ -108,8 +109,8 @@ class CordaClassResolverTests { val emptyMapClass = mapOf().javaClass } - private val emptyWhitelistContext: SerializationContext = SerializationContextImpl(kryoMagic, this.javaClass.classLoader, EmptyWhitelist, emptyMap(), true, SerializationContext.UseCase.P2P) - private val allButBlacklistedContext: SerializationContext = SerializationContextImpl(kryoMagic, this.javaClass.classLoader, AllButBlacklisted, emptyMap(), true, SerializationContext.UseCase.P2P) + private val emptyWhitelistContext: SerializationContext = SerializationContextImpl(kryoMagic, this.javaClass.classLoader, EmptyWhitelist, emptyMap(), true, SerializationContext.UseCase.P2P, null) + private val allButBlacklistedContext: SerializationContext = SerializationContextImpl(kryoMagic, this.javaClass.classLoader, AllButBlacklisted, emptyMap(), true, SerializationContext.UseCase.P2P, null) @Test fun `Annotation on enum works for specialised entries`() { CordaClassResolver(emptyWhitelistContext).getRegistration(Foo.Bar::class.java) @@ -195,7 +196,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`() { @@ -206,6 +207,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) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/KryoTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/KryoTests.kt index 5d54a38fbe..7150e8c566 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/KryoTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/KryoTests.kt @@ -1,10 +1,13 @@ package net.corda.nodeapi.internal.serialization import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.KryoException import com.esotericsoftware.kryo.KryoSerializable import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output import com.google.common.primitives.Ints +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.whenever import net.corda.core.contracts.PrivacySalt import net.corda.core.crypto.* import net.corda.core.internal.FetchDataFlow @@ -16,24 +19,29 @@ import net.corda.node.services.persistence.NodeAttachmentService import net.corda.nodeapi.internal.serialization.kryo.kryoMagic import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.TestIdentity -import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.api.Assertions.assertThatThrownBy +import net.corda.testing.internal.rigorousMock +import org.assertj.core.api.Assertions.* import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameters import org.slf4j.LoggerFactory import java.io.ByteArrayInputStream import java.io.InputStream import java.time.Instant import java.util.* -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotNull -import kotlin.test.assertTrue +import kotlin.test.* -class KryoTests { +@RunWith(Parameterized::class) +class KryoTests(private val compression: CordaSerializationEncoding?) { companion object { private val ALICE_PUBKEY = TestIdentity(ALICE_NAME, 70).publicKey + @Parameters(name = "{0}") + @JvmStatic + fun compression() = arrayOf(null) + CordaSerializationEncoding.values() } private lateinit var factory: SerializationFactory @@ -47,7 +55,11 @@ class KryoTests { AllWhitelist, emptyMap(), true, - SerializationContext.UseCase.Storage) + SerializationContext.UseCase.Storage, + compression, + rigorousMock().also { + if (compression != null) doReturn(true).whenever(it).acceptEncoding(compression) + }) } @Test @@ -259,7 +271,8 @@ class KryoTests { AllWhitelist, emptyMap(), true, - SerializationContext.UseCase.P2P) + SerializationContext.UseCase.P2P, + null) pt.serialize(factory, context) } @@ -300,4 +313,24 @@ class KryoTests { val exception2 = exception.serialize(factory, context).deserialize(factory, context) assertEquals(randomHash, exception2.requested) } + + @Test + fun `compression has the desired effect`() { + compression ?: return + val data = ByteArray(12345).also { Random(0).nextBytes(it) }.let { it + it } + val compressed = data.serialize(factory, context) + assertEquals(.5, compressed.size.toDouble() / data.size, .03) + assertArrayEquals(data, compressed.deserialize(factory, context)) + } + + @Test + fun `a particular encoding can be banned for deserialization`() { + compression ?: return + doReturn(false).whenever(context.encodingWhitelist).acceptEncoding(compression) + val compressed = "whatever".serialize(factory, context) + catchThrowable { compressed.deserialize(factory, context) }.run { + assertSame(KryoException::class.java, javaClass) + assertEquals(encodingNotPermittedFormat.format(compression), message) + } + } } \ No newline at end of file diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ListsSerializationTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ListsSerializationTest.kt index dc53b8fc29..7e1ffac95a 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ListsSerializationTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ListsSerializationTest.kt @@ -69,6 +69,7 @@ class ListsSerializationTest { val serializedForm = emptyList().serialize() val output = ByteArrayOutputStream().apply { kryoMagic.writeTo(this) + SectionId.ALT_DATA_AND_STOP.writeTo(this) write(DefaultClassResolver.NAME + 2) write(nameID) write(javaEmptyListClass.name.toAscii()) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/MapsSerializationTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/MapsSerializationTest.kt index a76bb8a52e..8efb66fffd 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/MapsSerializationTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/MapsSerializationTest.kt @@ -79,6 +79,7 @@ class MapsSerializationTest { val serializedForm = emptyMap().serialize() val output = ByteArrayOutputStream().apply { kryoMagic.writeTo(this) + SectionId.ALT_DATA_AND_STOP.writeTo(this) write(DefaultClassResolver.NAME + 2) write(nameID) write(javaEmptyMapClass.name.toAscii()) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SerializationTokenTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SerializationTokenTest.kt index 17495cb360..06a0f86d35 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SerializationTokenTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SerializationTokenTest.kt @@ -99,6 +99,7 @@ class SerializationTokenTest { val stream = ByteArrayOutputStream() Output(stream).use { kryoMagic.writeTo(it) + SectionId.ALT_DATA_AND_STOP.writeTo(it) kryo.writeClass(it, SingletonSerializeAsToken::class.java) kryo.writeObject(it, emptyList()) } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SetsSerializationTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SetsSerializationTest.kt index edd1eabf58..7d4a352323 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SetsSerializationTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SetsSerializationTest.kt @@ -56,6 +56,7 @@ class SetsSerializationTest { val serializedForm = emptySet().serialize() val output = ByteArrayOutputStream().apply { kryoMagic.writeTo(this) + SectionId.ALT_DATA_AND_STOP.writeTo(this) write(DefaultClassResolver.NAME + 2) write(nameID) write(javaEmptySetClass.name.toAscii()) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.kt index b48ae16941..74b175ae48 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.kt @@ -523,7 +523,7 @@ class EvolvabilityTests { val resource = "networkParams.." 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)) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt index 89b2dcbad4..f8aa4fc768 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt @@ -2,6 +2,8 @@ package net.corda.nodeapi.internal.serialization.amqp +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.whenever import net.corda.client.rpc.RPCException import net.corda.core.CordaRuntimeException import net.corda.core.contracts.* @@ -11,21 +13,16 @@ import net.corda.core.flows.FlowException import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name import net.corda.core.internal.AbstractAttachment -import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.MissingAttachmentsException -import net.corda.core.serialization.SerializationFactory +import net.corda.core.serialization.* import net.corda.core.transactions.LedgerTransaction import net.corda.core.utilities.OpaqueBytes -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.* 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 import net.corda.testing.core.TestIdentity +import net.corda.testing.internal.rigorousMock import org.apache.activemq.artemis.api.core.SimpleString import org.apache.qpid.proton.amqp.* import org.apache.qpid.proton.codec.DecoderImpl @@ -35,22 +32,23 @@ import org.junit.Assert.* import org.junit.Ignore import org.junit.Rule import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameters 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 import kotlin.test.assertTrue -class SerializationOutputTests { +@RunWith(Parameterized::class) +class SerializationOutputTests(private val compression: CordaSerializationEncoding?) { private companion object { val BOB_IDENTITY = TestIdentity(BOB_NAME, 80).identity val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")) @@ -59,6 +57,9 @@ class SerializationOutputTests { val MEGA_CORP_PUBKEY get() = megaCorp.publicKey val MINI_CORP get() = miniCorp.party val MINI_CORP_PUBKEY get() = miniCorp.publicKey + @Parameters(name = "{0}") + @JvmStatic + fun compression() = arrayOf(null) + CordaSerializationEncoding.values() } @Rule @@ -173,16 +174,20 @@ class SerializationOutputTests { } } + private val encodingWhitelist = rigorousMock().also { + if (compression != null) doReturn(true).whenever(it).acceptEncoding(compression) + } + + private fun defaultFactory() = SerializerFactory( + AllWhitelist, ClassLoader.getSystemClassLoader(), + EvolutionSerializerGetterTesting()) + private inline fun serdes(obj: T, - factory: SerializerFactory = SerializerFactory( - AllWhitelist, ClassLoader.getSystemClassLoader(), - EvolutionSerializerGetterTesting()), - freshDeserializationFactory: SerializerFactory = SerializerFactory( - AllWhitelist, ClassLoader.getSystemClassLoader(), - EvolutionSerializerGetterTesting()), + factory: SerializerFactory = defaultFactory(), + freshDeserializationFactory: SerializerFactory = defaultFactory(), expectedEqual: Boolean = true, expectDeserializedEqual: Boolean = true): T { - val ser = SerializationOutput(factory) + val ser = SerializationOutput(factory, compression) val bytes = ser.serialize(obj) val decoder = DecoderImpl().apply { @@ -198,18 +203,19 @@ class SerializationOutputTests { this.register(TransformTypes.DESCRIPTOR, TransformTypes.Companion) } EncoderImpl(decoder) - decoder.setByteBuffer(ByteBuffer.wrap(bytes.bytes, 8, bytes.size - 8)) - // Check that a vanilla AMQP decoder can deserialize without schema. - val result = decoder.readObject() as Envelope - assertNotNull(result) - - val des = DeserializationInput(freshDeserializationFactory) + DeserializationInput.withDataBytes(bytes, encodingWhitelist) { + decoder.setByteBuffer(it) + // Check that a vanilla AMQP decoder can deserialize without schema. + val result = decoder.readObject() as Envelope + assertNotNull(result) + } + val des = DeserializationInput(freshDeserializationFactory, encodingWhitelist) val desObj = des.deserialize(bytes) assertTrue(Objects.deepEquals(obj, desObj) == expectedEqual) // Now repeat with a re-used factory - val ser2 = SerializationOutput(factory) - val des2 = DeserializationInput(factory) + val ser2 = SerializationOutput(factory, compression) + val des2 = DeserializationInput(factory, encodingWhitelist) val desObj2 = des2.deserialize(ser2.serialize(obj)) assertTrue(Objects.deepEquals(obj, desObj2) == expectedEqual) assertTrue(Objects.deepEquals(desObj, desObj2) == expectDeserializedEqual) @@ -432,9 +438,9 @@ class SerializationOutputTests { @Test fun `class constructor is invoked on deserialisation`() { - val ser = SerializationOutput(SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())) - val des = DeserializationInput(ser.serializerFactory) - + compression == null || return // Manipulation of serialized bytes is invalid if they're compressed. + val ser = SerializationOutput(SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()), compression) + val des = DeserializationInput(ser.serializerFactory, encodingWhitelist) val serialisedOne = ser.serialize(NonZeroByte(1)).bytes val serialisedTwo = ser.serialize(NonZeroByte(2)).bytes @@ -1065,6 +1071,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()) } @@ -1116,6 +1123,29 @@ class SerializationOutputTests { val c = C(Amount(100, BigDecimal("1.5"), Currency.getInstance("USD"))) // were the issue not fixed we'd blow up here - SerializationOutput(factory).serialize(c) + SerializationOutput(factory, compression).serialize(c) + } + + @Test + fun `compression has the desired effect`() { + compression ?: return + val factory = defaultFactory() + val data = ByteArray(12345).also { Random(0).nextBytes(it) }.let { it + it } + val compressed = SerializationOutput(factory, compression).serialize(data) + assertEquals(.5, compressed.size.toDouble() / data.size, .03) + assertArrayEquals(data, DeserializationInput(factory, encodingWhitelist).deserialize(compressed)) + } + + @Test + fun `a particular encoding can be banned for deserialization`() { + compression ?: return + val factory = defaultFactory() + doReturn(false).whenever(encodingWhitelist).acceptEncoding(compression) + val compressed = SerializationOutput(factory, compression).serialize("whatever") + val input = DeserializationInput(factory, encodingWhitelist) + catchThrowable { input.deserialize(compressed) }.run { + assertSame(NotSerializableException::class.java, javaClass) + assertEquals(encodingNotPermittedFormat.format(compression), message) + } } } \ No newline at end of file diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/kryo/KryoStreamsTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/kryo/KryoStreamsTest.kt index ac9779f828..d8eedd305d 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/kryo/KryoStreamsTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/kryo/KryoStreamsTest.kt @@ -1,12 +1,16 @@ package net.corda.nodeapi.internal.serialization.kryo +import net.corda.core.internal.declaredField +import org.assertj.core.api.Assertions.catchThrowable import org.junit.Assert.assertArrayEquals import org.junit.Test import java.io.* +import java.nio.BufferOverflowException import java.util.* import java.util.zip.DeflaterOutputStream import java.util.zip.InflaterInputStream import kotlin.test.assertEquals +import kotlin.test.assertSame class KryoStreamsTest { class NegOutputStream(private val stream: OutputStream) : OutputStream() { @@ -57,4 +61,37 @@ class KryoStreamsTest { assertEquals(-1, read()) } } + + @Test + fun `ByteBufferOutputStream works`() { + val stream = ByteBufferOutputStream(3) + stream.write("abc".toByteArray()) + val getBuf = stream.declaredField(ByteArrayOutputStream::class, "buf")::value + assertEquals(3, getBuf().size) + repeat(2) { + assertSame(BufferOverflowException::class.java, catchThrowable { + stream.alsoAsByteBuffer(9) { + it.put("0123456789".toByteArray()) + } + }.javaClass) + assertEquals(3 + 9, getBuf().size) + } + // This time make too much space: + stream.alsoAsByteBuffer(11) { + it.put("0123456789".toByteArray()) + } + stream.write("def".toByteArray()) + assertArrayEquals("abc0123456789def".toByteArray(), stream.toByteArray()) + } + + @Test + fun `ByteBufferOutputStream discards data after final position`() { + val stream = ByteBufferOutputStream(0) + stream.alsoAsByteBuffer(10) { + it.put("0123456789".toByteArray()) + it.position(5) + } + stream.write("def".toByteArray()) + assertArrayEquals("01234def".toByteArray(), stream.toByteArray()) + } } diff --git a/node/build.gradle b/node/build.gradle index 2aeb7878d3..13dedeea6a 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -9,6 +9,14 @@ apply plugin: 'com.jfrog.artifactory' description 'Corda node modules' +// Import private compile time constants +buildscript { + def properties = new Properties() + file("$projectDir/src/main/resources/build.properties").withInputStream { properties.load(it) } + + ext.jolokia_version = properties.getProperty('jolokiaAgentVersion') +} + //noinspection GroovyAssignabilityCheck configurations { compile { @@ -159,9 +167,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}" @@ -172,6 +177,9 @@ dependencies { // Jsh: Testing SSH server integrationTestCompile group: 'com.jcraft', name: 'jsch', version: '0.1.54' + // AgentLoader: dynamic loading of JVM agents + compile group: 'com.ea.agentloader', name: 'ea-agent-loader', version: "${eaagentloader_version}" + // Jetty dependencies for NetworkMapClient test. // Web stuff: for HTTP[S] servlets testCompile "org.eclipse.jetty:jetty-servlet:${jetty_version}" @@ -183,7 +191,6 @@ dependencies { testCompile "org.glassfish.jersey.containers:jersey-container-servlet-core:${jersey_version}" testCompile "org.glassfish.jersey.containers:jersey-container-jetty-http:${jersey_version}" - // Jolokia JVM monitoring agent runtime "org.jolokia:jolokia-jvm:${jolokia_version}:agent" } diff --git a/node/capsule/build.gradle b/node/capsule/build.gradle index 3572e594fb..ca320e6b5d 100644 --- a/node/capsule/build.gradle +++ b/node/capsule/build.gradle @@ -59,11 +59,6 @@ task buildCordaJAR(type: FatCapsule, dependsOn: project(':node').compileJava) { // If you change these flags, please also update Driver.kt jvmArgs = ['-Xmx200m', '-XX:+UseG1GC'] } - - // Make the resulting JAR file directly executable on UNIX by prepending a shell script to it. - // This lets you run the file like so: ./corda.jar - // Other than being slightly less typing, this has one big advantage: Ctrl-C works properly in the terminal. - reallyExecutable { trampolining() } } artifacts { diff --git a/node/src/integration-test/kotlin/net/corda/node/CordappConfigFileProviderTests.kt b/node/src/integration-test/kotlin/net/corda/node/CordappConfigFileProviderTests.kt new file mode 100644 index 0000000000..ee2fd4674f --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/CordappConfigFileProviderTests.kt @@ -0,0 +1,60 @@ +package net.corda.node + +import com.typesafe.config.Config +import com.typesafe.config.ConfigException +import com.typesafe.config.ConfigFactory +import com.typesafe.config.ConfigRenderOptions +import net.corda.node.internal.cordapp.CordappConfigFileProvider +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import java.io.File +import java.nio.file.Files +import java.nio.file.Path + +class CordappConfigFileProviderTests { + private companion object { + val cordappConfDir = File("build/tmp/cordapps/config") + val cordappName = "test" + val cordappConfFile = File(cordappConfDir, cordappName + ".conf").toPath() + + val validConfig = ConfigFactory.parseString("key=value") + val alternateValidConfig = ConfigFactory.parseString("key=alternateValue") + val invalidConfig = "Invalid" + } + + val provider = CordappConfigFileProvider(cordappConfDir) + + @Test + fun `test that config can be loaded`() { + writeConfig(validConfig, cordappConfFile) + assertThat(provider.getConfigByName(cordappName)).isEqualTo(validConfig) + } + + @Test + fun `config is idempotent if the underlying file is not changed`() { + writeConfig(validConfig, cordappConfFile) + assertThat(provider.getConfigByName(cordappName)).isEqualTo(validConfig) + assertThat(provider.getConfigByName(cordappName)).isEqualTo(validConfig) + } + + @Test + fun `config is not idempotent if the underlying file is changed`() { + writeConfig(validConfig, cordappConfFile) + assertThat(provider.getConfigByName(cordappName)).isEqualTo(validConfig) + + writeConfig(alternateValidConfig, cordappConfFile) + assertThat(provider.getConfigByName(cordappName)).isEqualTo(alternateValidConfig) + } + + @Test(expected = ConfigException.Parse::class) + fun `an invalid config throws an exception`() { + Files.write(cordappConfFile, invalidConfig.toByteArray()) + + provider.getConfigByName(cordappName) + } + + /** + * Writes the config to the path provided - will (and must) overwrite any existing config + */ + private fun writeConfig(config: Config, to: Path) = Files.write(cordappConfFile, config.root().render(ConfigRenderOptions.concise()).toByteArray()) +} \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt b/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt index f13e73de06..39c0a8880f 100644 --- a/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt @@ -138,7 +138,7 @@ class AMQPBridgeTest { doReturn("trustpass").whenever(it).trustStorePassword doReturn("cordacadevpass").whenever(it).keyStorePassword doReturn(artemisAddress).whenever(it).p2pAddress - doReturn("").whenever(it).exportJMXto + doReturn(null).whenever(it).jmxMonitoringHttpPort doReturn(emptyList()).whenever(it).certificateChainCheckPolicies } artemisConfig.configureWithDevSSLCertificate() diff --git a/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt b/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt index 64d7c09990..8262f0b30b 100644 --- a/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt @@ -222,7 +222,7 @@ class ProtonWrapperTests { doReturn("trustpass").whenever(it).trustStorePassword doReturn("cordacadevpass").whenever(it).keyStorePassword doReturn(NetworkHostAndPort("0.0.0.0", artemisPort)).whenever(it).p2pAddress - doReturn("").whenever(it).exportJMXto + doReturn(null).whenever(it).jmxMonitoringHttpPort doReturn(emptyList()).whenever(it).certificateChainCheckPolicies } artemisConfig.configureWithDevSSLCertificate() diff --git a/node/src/integration-test/kotlin/net/corda/node/modes/draining/P2PFlowsDrainingModeTest.kt b/node/src/integration-test/kotlin/net/corda/node/modes/draining/P2PFlowsDrainingModeTest.kt index de79e99732..7afdae9343 100644 --- a/node/src/integration-test/kotlin/net/corda/node/modes/draining/P2PFlowsDrainingModeTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/modes/draining/P2PFlowsDrainingModeTest.kt @@ -18,8 +18,9 @@ import org.assertj.core.api.AssertionsForInterfaceTypes.assertThat import org.junit.After import org.junit.Before import org.junit.Test -import java.util.concurrent.ExecutorService import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit import kotlin.test.fail class P2PFlowsDrainingModeTest { @@ -28,7 +29,7 @@ class P2PFlowsDrainingModeTest { private val user = User("mark", "dadada", setOf(Permissions.all())) private val users = listOf(user) - private var executor: ExecutorService? = null + private var executor: ScheduledExecutorService? = null companion object { private val logger = loggerFor() @@ -36,7 +37,7 @@ class P2PFlowsDrainingModeTest { @Before fun setup() { - executor = Executors.newSingleThreadExecutor() + executor = Executors.newSingleThreadScheduledExecutor() } @After @@ -59,11 +60,11 @@ class P2PFlowsDrainingModeTest { initiating.apply { val flow = startFlow(::InitiateSessionFlow, counterParty) // this should be really fast, for the flow has already started, so 5 seconds should never be a problem - executor!!.submit({ + executor!!.schedule({ logger.info("Now disabling flows draining mode for $counterParty.") shouldFail = false initiated.setFlowsDrainingModeEnabled(false) - }) + }, 5, TimeUnit.SECONDS) flow.returnValue.map { result -> if (shouldFail) { fail("Shouldn't happen until flows draining mode is switched off.") diff --git a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt index 5fa206af81..166056f0bf 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt @@ -2,8 +2,8 @@ package net.corda.node.services import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.whenever -import net.corda.core.contracts.Contract -import net.corda.core.contracts.PartyAndReference +import net.corda.core.contracts.* +import net.corda.core.cordapp.CordappProvider import net.corda.core.flows.FlowLogic import net.corda.core.flows.UnexpectedFlowEndException import net.corda.core.identity.CordaX500Name @@ -12,7 +12,9 @@ import net.corda.core.internal.concurrent.transpose import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.internal.toLedgerTransaction +import net.corda.core.node.NetworkParameters import net.corda.core.node.ServicesForResolution +import net.corda.core.node.services.AttachmentStorage import net.corda.core.node.services.IdentityService import net.corda.core.serialization.SerializationFactory import net.corda.core.transactions.TransactionBuilder @@ -20,6 +22,7 @@ import net.corda.core.utilities.contextLogger import net.corda.core.utilities.getOrThrow 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_BANK_A_NAME import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.core.SerializationEnvironmentRule @@ -29,6 +32,7 @@ import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.driver import net.corda.testing.internal.rigorousMock import net.corda.testing.internal.withoutTestSerialization +import net.corda.testing.internal.MockCordappConfigProvider import net.corda.testing.services.MockAttachmentStorage import org.junit.Assert.assertEquals import org.junit.Rule @@ -42,7 +46,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)), MockCordappConfigProvider(), attachments, testNetworkParameters().whitelistedContractImplementations) private val cordapp get() = provider.cordapps.first() private val attachmentId get() = provider.getCordappAttachmentId(cordapp)!! private val appContext get() = provider.getAppContext(cordapp) @@ -78,12 +82,15 @@ class AttachmentLoadingTests { } } - private val services = rigorousMock().also { - doReturn(attachments).whenever(it).attachments - doReturn(provider).whenever(it).cordappProvider - doReturn(rigorousMock().also { - doReturn(null).whenever(it).partyFromKey(DUMMY_BANK_A.owningKey) - }).whenever(it).identityService + private val services = object : ServicesForResolution { + override fun loadState(stateRef: StateRef): TransactionState<*> = throw NotImplementedError() + override fun loadStates(stateRefs: Set): Set> = throw NotImplementedError() + override val identityService = rigorousMock().apply { + doReturn(null).whenever(this).partyFromKey(DUMMY_BANK_A.owningKey) + } + override val attachments: AttachmentStorage get() = this@AttachmentLoadingTests.attachments + override val cordappProvider: CordappProvider get() = this@AttachmentLoadingTests.provider + override val networkParameters: NetworkParameters = testNetworkParameters() } @Test diff --git a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt index e972d71468..60a03f6822 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt @@ -13,25 +13,26 @@ 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 import net.corda.core.utilities.Try import net.corda.core.utilities.getOrThrow +import net.corda.node.internal.StartedNode import net.corda.node.services.config.BFTSMaRtConfiguration import net.corda.node.services.config.NotaryConfig 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.MockNetwork -import net.corda.testing.node.StartedMockNode import net.corda.testing.node.MockNodeParameters +import net.corda.testing.node.internal.InternalMockNetwork +import net.corda.testing.node.internal.InternalMockNetwork.MockNode import net.corda.testing.node.startFlow import org.junit.After import org.junit.Before @@ -41,13 +42,13 @@ import kotlin.test.assertEquals import kotlin.test.assertTrue class BFTNotaryServiceTests { - private lateinit var mockNet: MockNetwork + private lateinit var mockNet: InternalMockNetwork private lateinit var notary: Party - private lateinit var node: StartedMockNode + private lateinit var node: StartedNode @Before fun before() { - mockNet = MockNetwork(emptyList()) + mockNet = InternalMockNetwork(listOf("net.corda.testing.contracts")) } @After @@ -153,7 +154,7 @@ class BFTNotaryServiceTests { } } - private fun StartedMockNode.signInitialTransaction(notary: Party, block: TransactionBuilder.() -> Any?): SignedTransaction { + private fun StartedNode.signInitialTransaction(notary: Party, block: TransactionBuilder.() -> Any?): SignedTransaction { return services.signInitialTransaction( TransactionBuilder(notary).apply { addCommand(dummyCommand(services.myInfo.chooseIdentity().owningKey)) diff --git a/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt index 20b07017ad..09f8733f1b 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt @@ -17,6 +17,7 @@ import net.corda.testing.driver.driver import net.corda.testing.core.dummyCommand import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.InProcess +import net.corda.testing.driver.internal.InProcessImpl import net.corda.testing.node.ClusterSpec import net.corda.testing.node.NotarySpec import net.corda.testing.node.startFlow @@ -62,7 +63,7 @@ class RaftNotaryServiceTests { } private fun issueState(nodeHandle: InProcess, notary: Party): StateAndRef<*> { - return nodeHandle.database.transaction { + return (nodeHandle as InProcessImpl).database.transaction { val builder = DummyContract.generateInitial(Random().nextInt(), notary, nodeHandle.services.myInfo.chooseIdentity().ref(0)) val stx = nodeHandle.services.signInitialTransaction(builder) diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt index a8c0f5d99b..b795a4da98 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt @@ -2,18 +2,20 @@ package net.corda.node.services.network import net.corda.cordform.CordformNode import net.corda.core.crypto.random63BitValue -import net.corda.core.internal.* import net.corda.core.internal.concurrent.transpose +import net.corda.core.internal.div +import net.corda.core.internal.exists +import net.corda.core.internal.list +import net.corda.core.internal.readObject import net.corda.core.node.NodeInfo -import net.corda.core.serialization.deserialize import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.seconds -import net.corda.core.node.NetworkParameters import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME +import net.corda.nodeapi.internal.network.SignedNetworkParameters +import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.core.SerializationEnvironmentRule -import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.PortAllocation import net.corda.testing.node.internal.CompatibilityZoneParams @@ -64,8 +66,7 @@ class NetworkMapTest { ) { val alice = startNode(providedName = ALICE_NAME).getOrThrow() val networkParameters = (alice.baseDirectory / NETWORK_PARAMS_FILE_NAME) - .readAll() - .deserialize>() + .readObject() .verified() // We use a random modified time above to make the network parameters unqiue so that we're sure they came // from the server diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt index 607852e7a8..8f7790b0e2 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt @@ -9,6 +9,8 @@ import net.corda.node.internal.Node import net.corda.node.internal.StartedNode import net.corda.testing.core.* import net.corda.testing.node.internal.NodeBasedTest +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.junit.Before import org.junit.Test import kotlin.test.assertEquals @@ -35,6 +37,45 @@ class PersistentNetworkMapCacheTest : NodeBasedTest() { } } + @Test + fun `unknown legal name`() { + val alice = startNodesWithPort(listOf(ALICE))[0] + val netMapCache = alice.services.networkMapCache + alice.database.transaction { + assertThat(netMapCache.getNodesByLegalName(DUMMY_NOTARY_NAME)).isEmpty() + assertThat(netMapCache.getNodeByLegalName(DUMMY_NOTARY_NAME)).isNull() + assertThat(netMapCache.getPeerByLegalName(DUMMY_NOTARY_NAME)).isNull() + assertThat(netMapCache.getPeerCertificateByLegalName(DUMMY_NOTARY_NAME)).isNull() + } + } + + @Test + fun `nodes in distributed service`() { + val alice = startNodesWithPort(listOf(ALICE))[0] + val netMapCache = alice.services.networkMapCache + + val distServiceNodeInfos = alice.database.transaction { + val distributedIdentity = TestIdentity(DUMMY_NOTARY_NAME).identity + (1..2).map { + val nodeInfo = NodeInfo( + addresses = listOf(NetworkHostAndPort("localhost", 1000 + it)), + legalIdentitiesAndCerts = listOf(TestIdentity.fresh("Org-$it").identity, distributedIdentity), + platformVersion = 3, + serial = 1 + ) + netMapCache.addNode(nodeInfo) + nodeInfo + } + } + + alice.database.transaction { + assertThat(netMapCache.getNodesByLegalName(DUMMY_NOTARY_NAME)).containsOnlyElementsOf(distServiceNodeInfos) + assertThatExceptionOfType(IllegalArgumentException::class.java) + .isThrownBy { netMapCache.getNodeByLegalName(DUMMY_NOTARY_NAME) } + .withMessageContaining(DUMMY_NOTARY_NAME.toString()) + } + } + @Test fun `get nodes by owning key and by name`() { val alice = startNodesWithPort(listOf(ALICE))[0] diff --git a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/LargeTransactionsTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/LargeTransactionsTest.kt index cd5f572d50..aa007be6d7 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/LargeTransactionsTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/LargeTransactionsTest.kt @@ -9,6 +9,8 @@ import net.corda.core.internal.concurrent.transpose import net.corda.core.messaging.startFlow import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.getOrThrow +import net.corda.node.services.config.MB +import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyState import net.corda.testing.core.* @@ -68,11 +70,15 @@ class LargeTransactionsTest { fun checkCanSendLargeTransactions() { // These 4 attachments yield a transaction that's got >10mb attached, so it'd push us over the Artemis // max message size. - val bigFile1 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024 * 3, 0) - val bigFile2 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024 * 3, 1) - val bigFile3 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024 * 3, 2) - val bigFile4 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024 * 3, 3) - driver(DriverParameters(startNodesInProcess = true, extraCordappPackagesToScan = listOf("net.corda.testing.contracts"), portAllocation = PortAllocation.RandomFree)) { + val bigFile1 = InputStreamAndHash.createInMemoryTestZip(3.MB.toInt(), 0) + val bigFile2 = InputStreamAndHash.createInMemoryTestZip(3.MB.toInt(), 1) + val bigFile3 = InputStreamAndHash.createInMemoryTestZip(3.MB.toInt(), 2) + val bigFile4 = InputStreamAndHash.createInMemoryTestZip(3.MB.toInt(), 3) + driver(DriverParameters( + startNodesInProcess = true, + extraCordappPackagesToScan = listOf("net.corda.testing.contracts"), + networkParameters = testNetworkParameters(maxTransactionSize = 13.MB.toInt()), + portAllocation = PortAllocation.RandomFree)) { val rpcUser = User("admin", "admin", setOf("ALL")) val (alice, _) = listOf(ALICE_NAME, BOB_NAME).map { startNode(providedName = it, rpcUsers = listOf(rpcUser)) }.transpose().getOrThrow() CordaRPCClient(alice.rpcAddress).use(rpcUser.username, rpcUser.password) { diff --git a/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt b/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt index 560987cdf7..c8ab1e3952 100644 --- a/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt @@ -16,10 +16,10 @@ import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_CA import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_INTERMEDIATE_CA import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA import net.corda.testing.common.internal.testNetworkParameters -import net.corda.testing.core.DEV_ROOT_CA import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.singleIdentity import net.corda.testing.driver.PortAllocation +import net.corda.testing.internal.DEV_ROOT_CA import net.corda.testing.node.NotarySpec import net.corda.testing.node.internal.CompatibilityZoneParams import net.corda.testing.node.internal.internalDriver diff --git a/node/src/main/kotlin/net/corda/node/ArgsParser.kt b/node/src/main/kotlin/net/corda/node/ArgsParser.kt index f42a922564..4f234a4fe8 100644 --- a/node/src/main/kotlin/net/corda/node/ArgsParser.kt +++ b/node/src/main/kotlin/net/corda/node/ArgsParser.kt @@ -3,7 +3,10 @@ package net.corda.node import com.typesafe.config.ConfigFactory import joptsimple.OptionParser import joptsimple.util.EnumConverter +import joptsimple.util.PathConverter +import net.corda.core.internal.CertRole import net.corda.core.internal.div +import net.corda.core.internal.exists import net.corda.node.services.config.ConfigHelper import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.parseAsNodeConfiguration @@ -34,9 +37,11 @@ class ArgsParser { private val sshdServerArg = optionParser.accepts("sshd", "Enables SSHD server for node administration.") private val noLocalShellArg = optionParser.accepts("no-local-shell", "Do not start the embedded shell locally.") private val isRegistrationArg = optionParser.accepts("initial-registration", "Start initial node registration with Corda network to obtain certificate from the permissioning server.") - private val networkRootTruststorePathArg = optionParser.accepts("network-root-truststore", "Network root trust store obtained from network operator.") + private val networkRootTrustStorePathArg = optionParser.accepts("network-root-truststore", "Network root trust store obtained from network operator.") .withRequiredArg() - private val networkRootTruststorePasswordArg = optionParser.accepts("network-root-truststore-password", "Network root trust store password obtained from network operator.") + .withValuesConvertedBy(PathConverter()) + .defaultsTo((Paths.get("certificates") / "network-root-truststore.jks")) + private val networkRootTrustStorePasswordArg = optionParser.accepts("network-root-truststore-password", "Network root trust store password obtained from network operator.") .withRequiredArg() private val isVersionArg = optionParser.accepts("version", "Print the version and exit") private val justGenerateNodeInfoArg = optionParser.accepts("just-generate-node-info", @@ -60,16 +65,23 @@ class ArgsParser { val sshdServer = optionSet.has(sshdServerArg) val justGenerateNodeInfo = optionSet.has(justGenerateNodeInfoArg) val bootstrapRaftCluster = optionSet.has(bootstrapRaftClusterArg) - val networkRootTruststorePath = optionSet.valueOf(networkRootTruststorePathArg)?.let { Paths.get(it).normalize().toAbsolutePath() } - val networkRootTruststorePassword = optionSet.valueOf(networkRootTruststorePasswordArg) + val networkRootTrustStorePath = optionSet.valueOf(networkRootTrustStorePathArg) + val networkRootTrustStorePassword = optionSet.valueOf(networkRootTrustStorePasswordArg) + + val registrationConfig = if (isRegistration) { + requireNotNull(networkRootTrustStorePassword) { "Network root trust store password must be provided in registration mode." } + require(networkRootTrustStorePath.exists()) { "Network root trust store path: '$networkRootTrustStorePath' doesn't exist" } + NodeRegistrationOption(networkRootTrustStorePath, networkRootTrustStorePassword) + } else { + null + } + return CmdLineOptions(baseDirectory, configFile, help, loggingLevel, logToConsole, - isRegistration, - networkRootTruststorePath, - networkRootTruststorePassword, + registrationConfig, isVersion, noLocalShell, sshdServer, @@ -80,14 +92,14 @@ class ArgsParser { fun printHelp(sink: PrintStream) = optionParser.printHelpOn(sink) } +data class NodeRegistrationOption(val networkRootTrustStorePath: Path, val networkRootTrustStorePassword: String) + data class CmdLineOptions(val baseDirectory: Path, val configFile: Path, val help: Boolean, val loggingLevel: Level, val logToConsole: Boolean, - val isRegistration: Boolean, - val networkRootTruststorePath: Path?, - val networkRootTruststorePassword: String?, + val nodeRegistrationConfig: NodeRegistrationOption?, val isVersion: Boolean, val noLocalShell: Boolean, val sshdServer: Boolean, @@ -97,10 +109,8 @@ data class CmdLineOptions(val baseDirectory: Path, val config = ConfigHelper.loadConfig(baseDirectory, configFile, configOverrides = ConfigFactory.parseMap( mapOf("noLocalShell" to this.noLocalShell) )).parseAsNodeConfiguration() - if (isRegistration) { + if (nodeRegistrationConfig != null) { requireNotNull(config.compatibilityZoneURL) { "Compatibility Zone Url must be provided in registration mode." } - requireNotNull(networkRootTruststorePath) { "Network root trust store path must be provided in registration mode." } - requireNotNull(networkRootTruststorePassword) { "Network root trust store password must be provided in registration mode." } } return config } diff --git a/node/src/main/kotlin/net/corda/node/internal/CordaClock.kt b/node/src/main/kotlin/net/corda/node/CordaClock.kt similarity index 98% rename from node/src/main/kotlin/net/corda/node/internal/CordaClock.kt rename to node/src/main/kotlin/net/corda/node/CordaClock.kt index c08961ee77..f838364bf6 100644 --- a/node/src/main/kotlin/net/corda/node/internal/CordaClock.kt +++ b/node/src/main/kotlin/net/corda/node/CordaClock.kt @@ -1,4 +1,4 @@ -package net.corda.node.internal +package net.corda.node import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SerializeAsTokenContext diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index a88bd73b5d..ca39b2b660 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -14,7 +14,6 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.FlowStateMachine -import net.corda.core.internal.GlobalProperties import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.concurrent.map import net.corda.core.internal.concurrent.openFuture @@ -30,8 +29,10 @@ import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.debug import net.corda.core.utilities.getOrThrow +import net.corda.node.CordaClock import net.corda.node.VersionInfo import net.corda.node.internal.classloading.requireAnnotation +import net.corda.node.internal.cordapp.CordappConfigFileProvider import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.node.internal.cordapp.CordappProviderInternal @@ -57,6 +58,8 @@ import net.corda.node.services.vault.NodeVaultService import net.corda.node.services.vault.VaultSoftLockManager import net.corda.node.shell.InteractiveShell import net.corda.node.utilities.AffinityExecutor +import net.corda.node.utilities.JVMAgentRegistry +import net.corda.node.utilities.NodeBuildProperties import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.persistence.CordaPersistence @@ -71,6 +74,7 @@ import rx.Observable import rx.Scheduler import java.io.IOException import java.lang.reflect.InvocationTargetException +import java.nio.file.Paths import java.security.KeyPair import java.security.KeyStoreException import java.security.PublicKey @@ -176,13 +180,13 @@ abstract class AbstractNode(val configuration: NodeConfiguration, // a code smell. val persistentNetworkMapCache = PersistentNetworkMapCache(database, notaries = emptyList()) persistentNetworkMapCache.start() - val (keyPairs, info) = initNodeInfo(persistentNetworkMapCache, identity, identityKeyPair) - val signedNodeInfo = info.sign { publicKey, serialised -> + val (keyPairs, nodeInfo) = initNodeInfo(persistentNetworkMapCache, identity, identityKeyPair) + val signedNodeInfo = nodeInfo.sign { publicKey, serialised -> val privateKey = keyPairs.single { it.public == publicKey }.private privateKey.sign(serialised.bytes) } NodeInfoWatcher.saveToFile(configuration.baseDirectory, signedNodeInfo) - info + nodeInfo } } @@ -190,22 +194,32 @@ abstract class AbstractNode(val configuration: NodeConfiguration, check(started == null) { "Node has already been started" } log.info("Node starting up ...") initCertificate() + initialiseJVMAgents() val schemaService = NodeSchemaService(cordappLoader.cordappSchemas, configuration.notary != null) val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null) val identityService = makeIdentityService(identity.certificate) networkMapClient = configuration.compatibilityZoneURL?.let { NetworkMapClient(it, identityService.trustRoot) } - GlobalProperties.networkParameters = NetworkParametersReader(identityService.trustRoot, networkMapClient, configuration.baseDirectory).networkParameters - check(GlobalProperties.networkParameters.minimumPlatformVersion <= versionInfo.platformVersion) { + val networkParameters = NetworkParametersReader(identityService.trustRoot, networkMapClient, configuration.baseDirectory).networkParameters + check(networkParameters.minimumPlatformVersion <= versionInfo.platformVersion) { "Node's platform version is lower than network's required minimumPlatformVersion" } // Do all of this in a database transaction so anything that might need a connection has one. val (startedImpl, schedulerService) = initialiseDatabasePersistence(schemaService, identityService) { database -> - val networkMapCache = NetworkMapCacheImpl(PersistentNetworkMapCache(database, GlobalProperties.networkParameters.notaries).start(), identityService) - val (keyPairs, info) = initNodeInfo(networkMapCache, identity, identityKeyPair) - identityService.loadIdentities(info.legalIdentitiesAndCerts) + val networkMapCache = NetworkMapCacheImpl(PersistentNetworkMapCache(database, networkParameters.notaries).start(), identityService) + val (keyPairs, nodeInfo) = initNodeInfo(networkMapCache, identity, identityKeyPair) + identityService.loadIdentities(nodeInfo.legalIdentitiesAndCerts) val transactionStorage = makeTransactionStorage(database, configuration.transactionCacheSizeBytes) val nodeProperties = NodePropertiesPersistentStore(StubbedNodeUniqueIdProvider::value, database) - val nodeServices = makeServices(keyPairs, schemaService, transactionStorage, database, info, identityService, networkMapCache, nodeProperties) + val nodeServices = makeServices( + keyPairs, + schemaService, + transactionStorage, + database, + nodeInfo, + identityService, + networkMapCache, + nodeProperties, + networkParameters) val notaryService = makeNotaryService(nodeServices, database) val smm = makeStateMachineManager(database) val flowLogicRefFactory = FlowLogicRefFactoryImpl(cordappLoader.appClassLoader) @@ -237,12 +251,12 @@ abstract class AbstractNode(val configuration: NodeConfiguration, registerCordappFlows(smm) _services.rpcFlows += cordappLoader.cordapps.flatMap { it.rpcFlows } startShell(rpcOps) - Pair(StartedNodeImpl(this, _services, info, checkpointStorage, smm, attachments, network, database, rpcOps, flowStarter, notaryService), schedulerService) + Pair(StartedNodeImpl(this, _services, nodeInfo, checkpointStorage, smm, attachments, network, database, rpcOps, flowStarter, notaryService), schedulerService) } networkMapUpdater = NetworkMapUpdater(services.networkMapCache, NodeInfoWatcher(configuration.baseDirectory, getRxIoScheduler(), Duration.ofMillis(configuration.additionalNodeInfoPollingFrequencyMsec)), networkMapClient, - GlobalProperties.networkParameters.serialize().hash, + networkParameters.serialize().hash, configuration.baseDirectory) runOnStop += networkMapUpdater::close @@ -295,20 +309,22 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } } - var info = NodeInfo( + val nodeInfoWithBlankSerial = NodeInfo( myAddresses(), setOf(identity, myNotaryIdentity).filterNotNull(), versionInfo.platformVersion, - platformClock.instant().toEpochMilli() + serial = 0 ) - // Check if we have already stored a version of 'our own' NodeInfo, this is to avoid regenerating it with - // a different timestamp. - networkMapCache.getNodesByLegalName(configuration.myLegalName).firstOrNull()?.let { - if (info.copy(serial = it.serial) == it) { - info = it - } + + val nodeInfoFromDb = networkMapCache.getNodeByLegalName(identity.name) + + val nodeInfo = if (nodeInfoWithBlankSerial == nodeInfoFromDb?.copy(serial = 0)) { + // The node info hasn't changed. We use the one from the database to preserve the serial. + nodeInfoFromDb + } else { + nodeInfoWithBlankSerial.copy(serial = platformClock.millis()) } - return Pair(keyPairs, info) + return Pair(keyPairs, nodeInfo) } protected abstract fun myAddresses(): List @@ -532,14 +548,15 @@ abstract class AbstractNode(val configuration: NodeConfiguration, schemaService: SchemaService, transactionStorage: WritableTransactionStorage, database: CordaPersistence, - info: NodeInfo, + nodeInfo: NodeInfo, identityService: IdentityServiceInternal, networkMapCache: NetworkMapCacheInternal, - nodeProperties: NodePropertiesStore): MutableList { + nodeProperties: NodePropertiesStore, + networkParameters: NetworkParameters): MutableList { checkpointStorage = DBCheckpointStorage() val metrics = MetricRegistry() attachments = NodeAttachmentService(metrics, configuration.attachmentContentCacheSizeBytes, configuration.attachmentCacheBound) - val cordappProvider = CordappProviderImpl(cordappLoader, attachments) + val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(), attachments, networkParameters.whitelistedContractImplementations) val keyManagementService = makeKeyManagementService(identityService, keyPairs) _services = ServiceHubInternalImpl( identityService, @@ -549,10 +566,11 @@ abstract class AbstractNode(val configuration: NodeConfiguration, MonitoringService(metrics), cordappProvider, database, - info, + nodeInfo, networkMapCache, - nodeProperties) - network = makeMessagingService(database, info, nodeProperties) + nodeProperties, + networkParameters) + network = makeMessagingService(database, nodeInfo, nodeProperties, networkParameters) val tokenizableServices = mutableListOf(attachments, network, services.vaultService, services.keyManagementService, services.identityService, platformClock, services.auditService, services.monitoringService, services.networkMapCache, services.schemaService, @@ -581,6 +599,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, log.warn("Certificate key store found but key store password does not match configuration.") false } catch (e: IOException) { + log.error("IO exception while trying to validate keystore", e) false } require(containCorrectKeys) { @@ -686,7 +705,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, _started = null } - protected abstract fun makeMessagingService(database: CordaPersistence, info: NodeInfo, nodeProperties: NodePropertiesStore): MessagingService + protected abstract fun makeMessagingService(database: CordaPersistence, info: NodeInfo, nodeProperties: NodePropertiesStore, networkParameters: NetworkParameters): MessagingService protected abstract fun startMessagingService(rpcOps: RPCOps) private fun obtainIdentity(notaryConfig: NotaryConfig?): Pair { @@ -745,6 +764,22 @@ abstract class AbstractNode(val configuration: NodeConfiguration, return NodeVaultService(platformClock, keyManagementService, stateLoader, hibernateConfig) } + /** Load configured JVM agents */ + private fun initialiseJVMAgents() { + configuration.jmxMonitoringHttpPort?.let { port -> + requireNotNull(NodeBuildProperties.JOLOKIA_AGENT_VERSION) { + "'jolokiaAgentVersion' missing from build properties" + } + log.info("Starting Jolokia agent on HTTP port: $port") + val libDir = Paths.get(configuration.baseDirectory.toString(), "drivers") + val jarFilePath = JVMAgentRegistry.resolveAgentJar( + "jolokia-jvm-${NodeBuildProperties.JOLOKIA_AGENT_VERSION}-agent.jar", libDir) ?: + throw Error("Unable to locate agent jar file") + log.info("Agent jar file: $jarFilePath") + JVMAgentRegistry.attach("jolokia", "port=$port", jarFilePath) + } + } + private inner class ServiceHubInternalImpl( override val identityService: IdentityService, // Place the long term identity key in the KMS. Eventually, this is likely going to be separated again because @@ -758,7 +793,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, override val database: CordaPersistence, override val myInfo: NodeInfo, override val networkMapCache: NetworkMapCacheInternal, - override val nodeProperties: NodePropertiesStore + override val nodeProperties: NodePropertiesStore, + override val networkParameters: NetworkParameters ) : SingletonSerializeAsToken(), ServiceHubInternal, StateLoader by validatedTransactions { override val rpcFlows = ArrayList>>() override val stateMachineRecordedTransactionMapping = DBTransactionMappingStorage() diff --git a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt index 0135895f29..6e46fb0d40 100644 --- a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt @@ -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 @@ -52,7 +53,7 @@ internal class CordaRPCOpsImpl( } override fun networkParametersFeed(): DataFeed { - return services.networkMapUpdater.track() + return services.networkMapUpdater.trackParametersUpdate() } override fun acceptNewNetworkParameters(parametersHash: SecureHash) { @@ -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) } } diff --git a/node/src/main/kotlin/net/corda/node/internal/NetworkParametersReader.kt b/node/src/main/kotlin/net/corda/node/internal/NetworkParametersReader.kt index f34263f707..bc5073ff4e 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NetworkParametersReader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NetworkParametersReader.kt @@ -3,12 +3,12 @@ package net.corda.node.internal import net.corda.core.crypto.SecureHash import net.corda.core.internal.* import net.corda.core.node.NetworkParameters -import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.utilities.contextLogger import net.corda.node.services.network.NetworkMapClient import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME import net.corda.nodeapi.internal.network.NETWORK_PARAMS_UPDATE_FILE_NAME +import net.corda.nodeapi.internal.network.SignedNetworkParameters import net.corda.nodeapi.internal.network.verifiedNetworkMapCert import java.nio.file.Path import java.nio.file.StandardCopyOption @@ -26,9 +26,9 @@ class NetworkParametersReader(private val trustRoot: X509Certificate, val networkParameters by lazy { retrieveNetworkParameters() } private fun retrieveNetworkParameters(): NetworkParameters { - val advertisedParametersHash = networkMapClient?.getNetworkMap()?.networkMap?.networkParameterHash + val advertisedParametersHash = networkMapClient?.getNetworkMap()?.payload?.networkParameterHash val signedParametersFromFile = if (networkParamsFile.exists()) { - networkParamsFile.readAll().deserialize>() + networkParamsFile.readObject() } else { null } @@ -51,13 +51,13 @@ class NetworkParametersReader(private val trustRoot: X509Certificate, return parameters } - private fun readParametersUpdate(advertisedParametersHash: SecureHash, previousParametersHash: SecureHash): SignedDataWithCert { + private fun readParametersUpdate(advertisedParametersHash: SecureHash, previousParametersHash: SecureHash): SignedNetworkParameters { if (!parametersUpdateFile.exists()) { throw IllegalArgumentException("Node uses parameters with hash: $previousParametersHash " + - "but network map is advertising: ${advertisedParametersHash}.\n" + + "but network map is advertising: $advertisedParametersHash.\n" + "Please update node to use correct network parameters file.") } - val signedUpdatedParameters = parametersUpdateFile.readAll().deserialize>() + val signedUpdatedParameters = parametersUpdateFile.readObject() if (signedUpdatedParameters.raw.hash != advertisedParametersHash) { throw IllegalArgumentException("Both network parameters and network parameters update files don't match" + "parameters advertised by network map.\n" + diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index 910c454e74..d15d749d05 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -2,12 +2,12 @@ package net.corda.node.internal import com.codahale.metrics.JmxReporter import net.corda.core.concurrent.CordaFuture -import net.corda.core.internal.GlobalProperties.networkParameters import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.concurrent.thenMatch import net.corda.core.internal.div import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.RPCOps +import net.corda.core.node.NetworkParameters import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub import net.corda.core.node.services.IdentityService @@ -16,6 +16,8 @@ import net.corda.core.serialization.internal.SerializationEnvironmentImpl import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger +import net.corda.node.CordaClock +import net.corda.node.SimpleClock import net.corda.node.VersionInfo import net.corda.node.internal.artemis.ArtemisBroker import net.corda.node.internal.artemis.BrokerAddresses @@ -145,7 +147,10 @@ open class Node(configuration: NodeConfiguration, private var shutdownHook: ShutdownHook? = null - override fun makeMessagingService(database: CordaPersistence, info: NodeInfo, nodeProperties: NodePropertiesStore): MessagingService { + override fun makeMessagingService(database: CordaPersistence, + info: NodeInfo, + nodeProperties: NodePropertiesStore, + networkParameters: NetworkParameters): MessagingService { // Construct security manager reading users data either from the 'security' config section // if present or from rpcUsers list if the former is missing from config. val securityManagerConfig = configuration.security?.authService ?: @@ -153,9 +158,13 @@ open class Node(configuration: NodeConfiguration, securityManager = RPCSecurityManagerImpl(securityManagerConfig) - val serverAddress = configuration.messagingServerAddress ?: makeLocalMessageBroker() - val rpcServerAddresses = if (configuration.rpcOptions.standAloneBroker) BrokerAddresses(configuration.rpcOptions.address!!, configuration.rpcOptions.adminAddress) else startLocalRpcBroker() - val advertisedAddress = info.addresses.single() + val serverAddress = configuration.messagingServerAddress ?: makeLocalMessageBroker(networkParameters) + val rpcServerAddresses = if (configuration.rpcOptions.standAloneBroker) { + BrokerAddresses(configuration.rpcOptions.address!!, configuration.rpcOptions.adminAddress) + } else { + startLocalRpcBroker(networkParameters) + } + val advertisedAddress = info.addresses[0] bridgeControlListener = BridgeControlListener(configuration, serverAddress, networkParameters.maxMessageSize) printBasicNodeInfo("Incoming connection address", advertisedAddress.toString()) @@ -183,16 +192,31 @@ open class Node(configuration: NodeConfiguration, drainingModeWasChangedEvents = nodeProperties.flowsDrainingMode.values) } - private fun startLocalRpcBroker(): BrokerAddresses? { + private fun startLocalRpcBroker(networkParameters: NetworkParameters): BrokerAddresses? { with(configuration) { return rpcOptions.address?.let { require(rpcOptions.address != null) { "RPC address needs to be specified for local RPC broker." } val rpcBrokerDirectory: Path = baseDirectory / "brokers" / "rpc" with(rpcOptions) { rpcBroker = if (useSsl) { - ArtemisRpcBroker.withSsl(this.address!!, sslConfig, securityManager, certificateChainCheckPolicies, networkParameters.maxMessageSize, exportJMXto.isNotEmpty(), rpcBrokerDirectory) + ArtemisRpcBroker.withSsl( + this.address!!, + sslConfig, + securityManager, + certificateChainCheckPolicies, + networkParameters.maxMessageSize, + jmxMonitoringHttpPort != null, + rpcBrokerDirectory) } else { - ArtemisRpcBroker.withoutSsl(this.address!!, adminAddress!!, sslConfig, securityManager, certificateChainCheckPolicies, networkParameters.maxMessageSize, exportJMXto.isNotEmpty(), rpcBrokerDirectory) + ArtemisRpcBroker.withoutSsl( + this.address!!, + adminAddress!!, + sslConfig, + securityManager, + certificateChainCheckPolicies, + networkParameters.maxMessageSize, + jmxMonitoringHttpPort != null, + rpcBrokerDirectory) } } return rpcBroker!!.addresses @@ -200,16 +224,14 @@ open class Node(configuration: NodeConfiguration, } } - private fun makeLocalMessageBroker(): NetworkHostAndPort { + private fun makeLocalMessageBroker(networkParameters: NetworkParameters): NetworkHostAndPort { with(configuration) { messageBroker = ArtemisMessagingServer(this, p2pAddress.port, networkParameters.maxMessageSize) return NetworkHostAndPort("localhost", p2pAddress.port) } } - override fun myAddresses(): List { - return listOf(configuration.messagingServerAddress ?: getAdvertisedAddress()) - } + override fun myAddresses(): List = listOf(getAdvertisedAddress()) private fun getAdvertisedAddress(): NetworkHostAndPort { return with(configuration) { diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt index 99fe31d1fc..17e980ee87 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -99,9 +99,9 @@ open class NodeStartup(val args: Array) { try { banJavaSerialisation(conf) preNetworkRegistration(conf) - if (shouldRegisterWithNetwork(cmdlineOptions, conf)) { + if (cmdlineOptions.nodeRegistrationConfig != null) { // Null checks for [compatibilityZoneURL], [rootTruststorePath] and [rootTruststorePassword] has been done in [CmdLineOptions.loadConfig] - registerWithNetwork(conf, cmdlineOptions.networkRootTruststorePath!!, cmdlineOptions.networkRootTruststorePassword!!) + registerWithNetwork(conf, cmdlineOptions.nodeRegistrationConfig) return true } logStartupInfo(versionInfo, cmdlineOptions, conf) @@ -184,12 +184,7 @@ open class NodeStartup(val args: Array) { logger.info("Starting as node on ${conf.p2pAddress}") } - private fun shouldRegisterWithNetwork(cmdlineOptions: CmdLineOptions, conf: NodeConfiguration): Boolean { - val compatibilityZoneURL = conf.compatibilityZoneURL - return !(!cmdlineOptions.isRegistration || compatibilityZoneURL == null) - } - - open protected fun registerWithNetwork(conf: NodeConfiguration, networkRootTruststorePath: Path, networkRootTruststorePassword: String) { + open protected fun registerWithNetwork(conf: NodeConfiguration, nodeRegistrationConfig: NodeRegistrationOption) { val compatibilityZoneURL = conf.compatibilityZoneURL!! println() println("******************************************************************") @@ -197,7 +192,7 @@ open class NodeStartup(val args: Array) { println("* Registering as a new participant with Corda network *") println("* *") println("******************************************************************") - NetworkRegistrationHelper(conf, HTTPNetworkRegistrationService(compatibilityZoneURL), networkRootTruststorePath, networkRootTruststorePassword).buildKeystore() + NetworkRegistrationHelper(conf, HTTPNetworkRegistrationService(compatibilityZoneURL), nodeRegistrationConfig).buildKeystore() } open protected fun loadConfigFile(cmdlineOptions: CmdLineOptions): NodeConfiguration = cmdlineOptions.loadConfig() diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappConfigFileProvider.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappConfigFileProvider.kt new file mode 100644 index 0000000000..921b67f98a --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappConfigFileProvider.kt @@ -0,0 +1,35 @@ +package net.corda.node.internal.cordapp + +import com.typesafe.config.Config +import com.typesafe.config.ConfigFactory +import net.corda.core.internal.cordapp.CordappConfigProvider +import net.corda.core.utilities.loggerFor +import java.io.File + +class CordappConfigFileProvider(val configDir: File = DEFAULT_CORDAPP_CONFIG_DIR) : CordappConfigProvider { + companion object { + val DEFAULT_CORDAPP_CONFIG_DIR = File("cordapps/config") + val CONFIG_EXT = ".conf" + val logger = loggerFor() + } + + init { + configDir.mkdirs() + } + + override fun getConfigByName(name: String): Config { + val configFile = File(configDir, name + CONFIG_EXT) + return if (configFile.exists()) { + if (configFile.isDirectory) { + throw IllegalStateException("File at ${configFile.absolutePath} is a directory, expected a config file") + } else { + logger.info("Found config for cordapp $name in ${configFile.absolutePath}") + ConfigFactory.parseFile(configFile) + } + } else { + logger.info("No config found for cordapp $name in ${configFile.absolutePath}") + ConfigFactory.empty() + } + } + +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappConfigProvider.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappConfigProvider.kt new file mode 100644 index 0000000000..f632481d1c --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappConfigProvider.kt @@ -0,0 +1,7 @@ +package net.corda.core.internal.cordapp + +import com.typesafe.config.Config + +interface CordappConfigProvider { + fun getConfigByName(name: String): Config +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt index cfed6d0cd2..06efde8cd7 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt @@ -1,25 +1,64 @@ 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.internal.cordapp.CordappConfigProvider +import net.corda.core.internal.createCordappContext import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.AttachmentStorage import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.loggerFor import java.net.URL +import java.util.concurrent.ConcurrentHashMap /** * 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, + private val cordappConfigProvider: CordappConfigProvider, + attachmentStorage: AttachmentStorage, + private val whitelistedContractImplementations: Map>) : SingletonSerializeAsToken(), CordappProviderInternal { companion object { private val log = loggerFor() } + private val contextCache = ConcurrentHashMap() + + /** + * 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 +75,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 +83,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 { - 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 = + 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 @@ -62,7 +101,14 @@ open class CordappProviderImpl(private val cordappLoader: CordappLoader, attachm * @return A cordapp context for the given CorDapp */ fun getAppContext(cordapp: Cordapp): CordappContext { - return CordappContext(cordapp, getCordappAttachmentId(cordapp), cordappLoader.appClassLoader) + return contextCache.computeIfAbsent(cordapp, { + createCordappContext( + cordapp, + getCordappAttachmentId(cordapp), + cordappLoader.appClassLoader, + TypesafeCordappConfig(cordappConfigProvider.getConfigByName(cordapp.name)) + ) + }) } /** diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/TypesafeCordappConfig.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/TypesafeCordappConfig.kt new file mode 100644 index 0000000000..73f5633350 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/TypesafeCordappConfig.kt @@ -0,0 +1,79 @@ +package net.corda.node.internal.cordapp + +import com.typesafe.config.Config +import com.typesafe.config.ConfigException +import net.corda.core.cordapp.CordappConfig +import net.corda.core.cordapp.CordappConfigException + +/** + * Provides configuration from a typesafe config source + */ +class TypesafeCordappConfig(private val cordappConfig: Config) : CordappConfig { + override fun exists(path: String): Boolean { + return cordappConfig.hasPath(path) + } + + override fun get(path: String): Any { + try { + return cordappConfig.getAnyRef(path) + } catch (e: ConfigException) { + throw CordappConfigException("Cordapp configuration is incorrect due to exception", e) + } + } + + override fun getInt(path: String): Int { + try { + return cordappConfig.getInt(path) + } catch (e: ConfigException) { + throw CordappConfigException("Cordapp configuration is incorrect due to exception", e) + } + } + + override fun getLong(path: String): Long { + try { + return cordappConfig.getLong(path) + } catch (e: ConfigException) { + throw CordappConfigException("Cordapp configuration is incorrect due to exception", e) + } + } + + override fun getFloat(path: String): Float { + try { + return cordappConfig.getDouble(path).toFloat() + } catch (e: ConfigException) { + throw CordappConfigException("Cordapp configuration is incorrect due to exception", e) + } + } + + override fun getDouble(path: String): Double { + try { + return cordappConfig.getDouble(path) + } catch (e: ConfigException) { + throw CordappConfigException("Cordapp configuration is incorrect due to exception", e) + } + } + + override fun getNumber(path: String): Number { + try { + return cordappConfig.getNumber(path) + } catch (e: ConfigException) { + throw CordappConfigException("Cordapp configuration is incorrect due to exception", e) + } + } + + override fun getString(path: String): String { + try { + return cordappConfig.getString(path) + } catch (e: ConfigException) { + throw CordappConfigException("Cordapp configuration is incorrect due to exception", e) + } + } + + override fun getBoolean(path: String): Boolean { + try { + return cordappConfig.getBoolean(path) + } catch (e: ConfigException) { + throw CordappConfigException("Cordapp configuration is incorrect due to exception", e) + } + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index 450bab72a4..02a5489d6b 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -25,7 +25,7 @@ val Int.MB: Long get() = this * 1024L * 1024L interface NodeConfiguration : NodeSSLConfiguration { val myLegalName: CordaX500Name val emailAddress: String - val exportJMXto: String + val jmxMonitoringHttpPort: Int? val dataSourceProperties: Properties val rpcUsers: List val security: SecurityConfiguration? @@ -118,6 +118,7 @@ data class NodeConfigurationImpl( /** This is not retrieved from the config file but rather from a command line argument. */ override val baseDirectory: Path, override val myLegalName: CordaX500Name, + override val jmxMonitoringHttpPort: Int? = null, override val emailAddress: String, override val keyStorePassword: String, override val trustStorePassword: String, @@ -170,21 +171,20 @@ data class NodeConfigurationImpl( override fun validate(): List { val errors = mutableListOf() - errors + validateRpcOptions(rpcOptions) + errors += validateRpcOptions(rpcOptions) return errors } private fun validateRpcOptions(options: NodeRpcOptions): List { val errors = mutableListOf() - if (!options.useSsl) { - if (options.adminAddress == null) { - errors + "'rpcSettings.adminAddress': missing. Property is mandatory when 'rpcSettings.useSsl' is false (default)." + if (options.address != null) { + if (!options.useSsl && options.adminAddress == null) { + errors += "'rpcSettings.adminAddress': missing. Property is mandatory when 'rpcSettings.useSsl' is false (default)." } } return errors } - override val exportJMXto: String get() = "http" override val transactionCacheSizeBytes: Long get() = transactionCacheSizeMegaBytes?.MB ?: super.transactionCacheSizeBytes override val attachmentContentCacheSizeBytes: Long diff --git a/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt b/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt index edc508c0e3..b79d7b3acd 100644 --- a/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt +++ b/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt @@ -21,8 +21,8 @@ import net.corda.core.schemas.PersistentStateRef import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.contextLogger import net.corda.core.utilities.trace -import net.corda.node.internal.CordaClock -import net.corda.node.internal.MutableClock +import net.corda.node.CordaClock +import net.corda.node.MutableClock import net.corda.node.services.api.FlowStarter import net.corda.node.services.api.NodePropertiesStore import net.corda.node.services.api.SchedulerService diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt index f6837b80c1..ab07903a1e 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt @@ -144,7 +144,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration, managementNotificationAddress = SimpleString(NOTIFICATIONS_ADDRESS) // JMX enablement - if (config.exportJMXto.isNotEmpty()) { + if (config.jmxMonitoringHttpPort != null) { isJMXManagementEnabled = true isJMXUseBrokerName = true } diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt index 0828efbe0e..5f050d2ec5 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt @@ -667,8 +667,8 @@ private class P2PMessagingConsumer( private val drainingModeWasChangedEvents: Observable>) : LifecycleSupport { private companion object { - private const val initialSessionMessages = "${P2PMessagingHeaders.Type.KEY}=${P2PMessagingHeaders.Type.SESSION_INIT_VALUE}" - private const val existingSessionMessages = "${P2PMessagingHeaders.Type.KEY}<>${P2PMessagingHeaders.Type.SESSION_INIT_VALUE}" + private const val initialSessionMessages = "${P2PMessagingHeaders.Type.KEY}='${P2PMessagingHeaders.Type.SESSION_INIT_VALUE}'" + private const val existingSessionMessages = "${P2PMessagingHeaders.Type.KEY}<>'${P2PMessagingHeaders.Type.SESSION_INIT_VALUE}'" } private var startedFlag = false diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt index 0e06674aa0..db3d90bef5 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt @@ -18,9 +18,7 @@ import net.corda.core.context.InvocationContext import net.corda.core.context.Trace import net.corda.core.context.Trace.InvocationId import net.corda.core.identity.CordaX500Name -import net.corda.core.internal.LazyStickyPool import net.corda.core.internal.LifeCycle -import net.corda.core.internal.join import net.corda.core.messaging.RPCOps import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationDefaults.RPC_SERVER_CONTEXT @@ -29,14 +27,17 @@ import net.corda.core.utilities.* import net.corda.node.internal.security.AuthorizingSubject import net.corda.node.internal.security.RPCSecurityManager import net.corda.node.services.logging.pushToLoggingContext -import net.corda.nodeapi.* +import net.corda.nodeapi.RPCApi +import net.corda.nodeapi.externalTrace +import net.corda.nodeapi.impersonatedActor +import net.corda.nodeapi.internal.DeduplicationChecker +import net.corda.nodeapi.internal.persistence.CordaPersistence +import net.corda.nodeapi.internal.persistence.contextDatabase +import net.corda.nodeapi.internal.persistence.contextDatabaseOrNull import org.apache.activemq.artemis.api.core.Message import org.apache.activemq.artemis.api.core.SimpleString +import org.apache.activemq.artemis.api.core.client.* import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE -import org.apache.activemq.artemis.api.core.client.ClientConsumer -import org.apache.activemq.artemis.api.core.client.ClientMessage -import org.apache.activemq.artemis.api.core.client.ClientSession -import org.apache.activemq.artemis.api.core.client.ServerLocator import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl import org.apache.activemq.artemis.api.core.management.CoreNotificationType import org.apache.activemq.artemis.api.core.management.ManagementHelper @@ -49,24 +50,23 @@ import rx.Subscription import java.lang.reflect.InvocationTargetException import java.lang.reflect.Method import java.time.Duration +import java.util.* import java.util.concurrent.* +import kotlin.concurrent.thread data class RPCServerConfiguration( /** The number of threads to use for handling RPC requests */ val rpcThreadPoolSize: Int, - /** The number of consumers to handle incoming messages */ - val consumerPoolSize: Int, - /** The maximum number of producers to create to handle outgoing messages */ - val producerPoolBound: Int, /** The interval of subscription reaping */ - val reapInterval: Duration + val reapInterval: Duration, + /** The cache expiry of a deduplication watermark per client. */ + val deduplicationCacheExpiry: Duration ) { companion object { val default = RPCServerConfiguration( rpcThreadPoolSize = 4, - consumerPoolSize = 2, - producerPoolBound = 4, - reapInterval = 1.seconds + reapInterval = 1.seconds, + deduplicationCacheExpiry = 1.days ) } } @@ -115,22 +115,24 @@ class RPCServer( /** The scheduled reaper handle. */ private var reaperScheduledFuture: ScheduledFuture<*>? = null - private var observationSendExecutor: ExecutorService? = null + private var senderThread: Thread? = null private var rpcExecutor: ScheduledExecutorService? = null private var reaperExecutor: ScheduledExecutorService? = null - private val sessionAndConsumers = ArrayList(rpcConfiguration.consumerPoolSize) - private val sessionAndProducerPool = LazyStickyPool(rpcConfiguration.producerPoolBound) { - val sessionFactory = serverLocator.createSessionFactory() - val session = sessionFactory.createSession(rpcServerUsername, rpcServerPassword, false, true, true, false, DEFAULT_ACK_BATCH_SIZE) - session.start() - ArtemisProducer(sessionFactory, session, session.createProducer()) - } + private var sessionFactory: ClientSessionFactory? = null + private var producerSession: ClientSession? = null + private var consumerSession: ClientSession? = null + private var rpcProducer: ClientProducer? = null + private var rpcConsumer: ClientConsumer? = null private var clientBindingRemovalConsumer: ClientConsumer? = null private var clientBindingAdditionConsumer: ClientConsumer? = null private var serverControl: ActiveMQServerControl? = null private val responseMessageBuffer = ConcurrentHashMap() + private val sendJobQueue = LinkedBlockingQueue() + + private val deduplicationChecker = DeduplicationChecker(rpcConfiguration.deduplicationCacheExpiry) + private var deduplicationIdentity: String? = null init { val groupedMethods = ops.javaClass.declaredMethods.groupBy { it.name } @@ -154,16 +156,12 @@ class RPCServer( try { lifeCycle.requireState(State.UNSTARTED) log.info("Starting RPC server with configuration $rpcConfiguration") - observationSendExecutor = Executors.newFixedThreadPool( - 1, - ThreadFactoryBuilder().setNameFormat("rpc-observation-sender-%d").build() - ) + senderThread = startSenderThread() rpcExecutor = Executors.newScheduledThreadPool( rpcConfiguration.rpcThreadPoolSize, ThreadFactoryBuilder().setNameFormat("rpc-server-handler-pool-%d").build() ) - reaperExecutor = Executors.newScheduledThreadPool( - 1, + reaperExecutor = Executors.newSingleThreadScheduledExecutor( ThreadFactoryBuilder().setNameFormat("rpc-server-reaper-%d").build() ) reaperScheduledFuture = reaperExecutor!!.scheduleAtFixedRate( @@ -172,55 +170,85 @@ class RPCServer( rpcConfiguration.reapInterval.toMillis(), TimeUnit.MILLISECONDS ) - val sessions = createConsumerSessions() - createNotificationConsumers() + + sessionFactory = serverLocator.createSessionFactory() + producerSession = sessionFactory!!.createSession(rpcServerUsername, rpcServerPassword, false, true, true, false, DEFAULT_ACK_BATCH_SIZE) + createRpcProducer(producerSession!!) + consumerSession = sessionFactory!!.createSession(rpcServerUsername, rpcServerPassword, false, true, true, false, DEFAULT_ACK_BATCH_SIZE) + createRpcConsumer(consumerSession!!) + createNotificationConsumers(consumerSession!!) serverControl = activeMqServerControl + deduplicationIdentity = UUID.randomUUID().toString() lifeCycle.transition(State.UNSTARTED, State.STARTED) // We delay the consumer session start because Artemis starts delivering messages immediately, so we need to be // fully initialised. - sessions.forEach { - it.start() - } + producerSession!!.start() + consumerSession!!.start() } catch (exception: Throwable) { close() throw exception } } - private fun createConsumerSessions(): ArrayList { - val sessions = ArrayList() - for (i in 1..rpcConfiguration.consumerPoolSize) { - val sessionFactory = serverLocator.createSessionFactory() - val session = sessionFactory.createSession(rpcServerUsername, rpcServerPassword, false, true, true, false, DEFAULT_ACK_BATCH_SIZE) - val consumer = session.createConsumer(RPCApi.RPC_SERVER_QUEUE_NAME) - consumer.setMessageHandler(this@RPCServer::clientArtemisMessageHandler) - sessionAndConsumers.add(ArtemisConsumer(sessionFactory, session, consumer)) - sessions.add(session) - } - return sessions + private fun createRpcProducer(producerSession: ClientSession) { + rpcProducer = producerSession.createProducer() } - private fun createNotificationConsumers() { - clientBindingRemovalConsumer = sessionAndConsumers[0].session.createConsumer(RPCApi.RPC_CLIENT_BINDING_REMOVALS) + private fun createRpcConsumer(consumerSession: ClientSession) { + rpcConsumer = consumerSession.createConsumer(RPCApi.RPC_SERVER_QUEUE_NAME) + rpcConsumer!!.setMessageHandler(this::clientArtemisMessageHandler) + } + + private fun createNotificationConsumers(consumerSession: ClientSession) { + clientBindingRemovalConsumer = consumerSession.createConsumer(RPCApi.RPC_CLIENT_BINDING_REMOVALS) clientBindingRemovalConsumer!!.setMessageHandler(this::bindingRemovalArtemisMessageHandler) - clientBindingAdditionConsumer = sessionAndConsumers[0].session.createConsumer(RPCApi.RPC_CLIENT_BINDING_ADDITIONS) + clientBindingAdditionConsumer = consumerSession.createConsumer(RPCApi.RPC_CLIENT_BINDING_ADDITIONS) clientBindingAdditionConsumer!!.setMessageHandler(this::bindingAdditionArtemisMessageHandler) } + private fun startSenderThread(): Thread { + return thread(name = "rpc-server-sender", isDaemon = true) { + var deduplicationSequenceNumber = 0L + while (true) { + val job = sendJobQueue.poll() + when (job) { + is RpcSendJob.Send -> handleSendJob(deduplicationSequenceNumber++, job) + RpcSendJob.Stop -> return@thread + } + } + } + } + + private fun handleSendJob(sequenceNumber: Long, job: RpcSendJob.Send) { + try { + val artemisMessage = producerSession!!.createMessage(false) + if (job.database != null) { + contextDatabase = job.database + } + // We must do the serialisation here as any encountered Observables may already have events, which would + // trigger more sends. We must make sure that the root of the Observables (e.g. the RPC reply) is sent + // before any child observations. + job.message.writeToClientMessage(job.serializationContext, artemisMessage) + artemisMessage.putLongProperty(RPCApi.DEDUPLICATION_SEQUENCE_NUMBER_FIELD_NAME, sequenceNumber) + rpcProducer!!.send(job.clientAddress, artemisMessage) + log.debug { "<- RPC <- ${job.message}" } + } catch (throwable: Throwable) { + log.error("Failed to send message, kicking client. Message was ${job.message}", throwable) + serverControl!!.closeConsumerConnectionsForAddress(job.clientAddress.toString()) + invalidateClient(job.clientAddress) + } + } + fun close() { - observationSendExecutor?.join() + sendJobQueue.put(RpcSendJob.Stop) + senderThread?.join() reaperScheduledFuture?.cancel(false) rpcExecutor?.shutdownNow() reaperExecutor?.shutdownNow() securityManager.close() - sessionAndConsumers.forEach { - it.sessionFactory.close() - } + sessionFactory?.close() observableMap.invalidateAll() reapSubscriptions() - sessionAndProducerPool.close().forEach { - it.sessionFactory.close() - } lifeCycle.justTransition(State.FINISHED) } @@ -271,32 +299,43 @@ class RPCServer( lifeCycle.requireState(State.STARTED) val clientToServer = RPCApi.ClientToServer.fromClientMessage(artemisMessage) log.debug { "-> RPC -> $clientToServer" } - when (clientToServer) { - is RPCApi.ClientToServer.RpcRequest -> { - val arguments = Try.on { - clientToServer.serialisedArguments.deserialize>(context = RPC_SERVER_CONTEXT) - } - val context = artemisMessage.context(clientToServer.sessionId) - context.invocation.pushToLoggingContext() - when (arguments) { - is Try.Success -> { - rpcExecutor!!.submit { - val result = invokeRpc(context, clientToServer.methodName, arguments.value) - sendReply(clientToServer.replyId, clientToServer.clientAddress, result) + try { + when (clientToServer) { + is RPCApi.ClientToServer.RpcRequest -> { + val deduplicationSequenceNumber = artemisMessage.getLongProperty(RPCApi.DEDUPLICATION_SEQUENCE_NUMBER_FIELD_NAME) + if (deduplicationChecker.checkDuplicateMessageId( + identity = clientToServer.clientAddress, + sequenceNumber = deduplicationSequenceNumber + )) { + log.info("Message duplication detected, discarding message") + return + } + val arguments = Try.on { + clientToServer.serialisedArguments.deserialize>(context = RPC_SERVER_CONTEXT) + } + val context = artemisMessage.context(clientToServer.sessionId) + context.invocation.pushToLoggingContext() + when (arguments) { + is Try.Success -> { + rpcExecutor!!.submit { + val result = invokeRpc(context, clientToServer.methodName, arguments.value) + sendReply(clientToServer.replyId, clientToServer.clientAddress, result) + } + } + is Try.Failure -> { + // We failed to deserialise the arguments, route back the error + log.warn("Inbound RPC failed", arguments.exception) + sendReply(clientToServer.replyId, clientToServer.clientAddress, arguments) } } - is Try.Failure -> { - // We failed to deserialise the arguments, route back the error - log.warn("Inbound RPC failed", arguments.exception) - sendReply(clientToServer.replyId, clientToServer.clientAddress, arguments) - } + } + is RPCApi.ClientToServer.ObservablesClosed -> { + observableMap.invalidateAll(clientToServer.ids) } } - is RPCApi.ClientToServer.ObservablesClosed -> { - observableMap.invalidateAll(clientToServer.ids) - } + } finally { + artemisMessage.acknowledge() } - artemisMessage.acknowledge() } private fun invokeRpc(context: RpcAuthContext, methodName: String, arguments: List): Try { @@ -316,15 +355,16 @@ class RPCServer( } private fun sendReply(replyId: InvocationId, clientAddress: SimpleString, result: Try) { - val reply = RPCApi.ServerToClient.RpcReply(replyId, result) + val reply = RPCApi.ServerToClient.RpcReply( + id = replyId, + result = result, + deduplicationIdentity = deduplicationIdentity!! + ) val observableContext = ObservableContext( - replyId, observableMap, clientAddressToObservables, - clientAddress, - serverControl!!, - sessionAndProducerPool, - observationSendExecutor!! + deduplicationIdentity!!, + clientAddress ) val buffered = bufferIfQueueNotBound(clientAddress, reply, observableContext) @@ -370,6 +410,36 @@ class RPCServer( val targetLegalIdentity = message.getStringProperty(RPCApi.RPC_TARGET_LEGAL_IDENTITY)?.let(CordaX500Name.Companion::parse) ?: nodeLegalName return Pair(Actor(Id(validatedUser), securityManager.id, targetLegalIdentity), securityManager.buildSubject(validatedUser)) } + + /* + * We construct an observable context on each RPC request. If subsequently a nested Observable is encountered this + * same context is propagated by the instrumented KryoPool. This way all observations rooted in a single RPC will be + * muxed correctly. Note that the context construction itself is quite cheap. + */ + inner class ObservableContext( + val observableMap: ObservableSubscriptionMap, + val clientAddressToObservables: SetMultimap, + val deduplicationIdentity: String, + val clientAddress: SimpleString + ) { + private val serializationContextWithObservableContext = RpcServerObservableSerializer.createContext(this) + + fun sendMessage(serverToClient: RPCApi.ServerToClient) { + sendJobQueue.put(RpcSendJob.Send(contextDatabaseOrNull, clientAddress, serializationContextWithObservableContext, serverToClient)) + } + } + + private sealed class RpcSendJob { + data class Send( + // TODO HACK this is because during serialisation we subscribe to observables that may use + // DatabaseTransactionWrappingSubscriber which tries to access the current database, + val database: CordaPersistence?, + val clientAddress: SimpleString, + val serializationContext: SerializationContext, + val message: RPCApi.ServerToClient + ) : RpcSendJob() + object Stop : RpcSendJob() + } } // TODO replace this by creating a new CordaRPCImpl for each request, passing the context, after we fix Shell and WebServer @@ -417,45 +487,11 @@ class ObservableSubscription( typealias ObservableSubscriptionMap = Cache -// We construct an observable context on each RPC request. If subsequently a nested Observable is -// encountered this same context is propagated by the instrumented KryoPool. This way all -// observations rooted in a single RPC will be muxed correctly. Note that the context construction -// itself is quite cheap. -class ObservableContext( - val invocationId: InvocationId, - val observableMap: ObservableSubscriptionMap, - val clientAddressToObservables: SetMultimap, - val clientAddress: SimpleString, - val serverControl: ActiveMQServerControl, - val sessionAndProducerPool: LazyStickyPool, - val observationSendExecutor: ExecutorService -) { - private companion object { - private val log = contextLogger() - } - - private val serializationContextWithObservableContext = RpcServerObservableSerializer.createContext(this) - - fun sendMessage(serverToClient: RPCApi.ServerToClient) { - try { - sessionAndProducerPool.run(invocationId) { - val artemisMessage = it.session.createMessage(false) - serverToClient.writeToClientMessage(serializationContextWithObservableContext, artemisMessage) - it.producer.send(clientAddress, artemisMessage) - log.debug("<- RPC <- $serverToClient") - } - } catch (throwable: Throwable) { - log.error("Failed to send message, kicking client. Message was $serverToClient", throwable) - serverControl.closeConsumerConnectionsForAddress(clientAddress.toString()) - } - } -} - object RpcServerObservableSerializer : Serializer>() { private object RpcObservableContextKey private val log = LoggerFactory.getLogger(javaClass) - fun createContext(observableContext: ObservableContext): SerializationContext { + fun createContext(observableContext: RPCServer.ObservableContext): SerializationContext { return RPC_SERVER_CONTEXT.withProperty(RpcServerObservableSerializer.RpcObservableContextKey, observableContext) } @@ -465,7 +501,7 @@ object RpcServerObservableSerializer : Serializer>() { override fun write(kryo: Kryo, output: Output, observable: Observable<*>) { val observableId = InvocationId.newInstance() - val observableContext = kryo.context[RpcObservableContextKey] as ObservableContext + val observableContext = kryo.context[RpcObservableContextKey] as RPCServer.ObservableContext output.writeInvocationId(observableId) val observableWithSubscription = ObservableSubscription( // We capture [observableContext] in the subscriber. Note that all synchronisation/kryo borrowing @@ -474,9 +510,12 @@ object RpcServerObservableSerializer : Serializer>() { object : Subscriber>() { override fun onNext(observation: Notification<*>) { if (!isUnsubscribed) { - observableContext.observationSendExecutor.submit { - observableContext.sendMessage(RPCApi.ServerToClient.Observation(observableId, observation)) - } + val message = RPCApi.ServerToClient.Observation( + id = observableId, + content = observation, + deduplicationIdentity = observableContext.deduplicationIdentity + ) + observableContext.sendMessage(message) } } diff --git a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapClient.kt b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapClient.kt index 2b56d933c4..617a03a39f 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapClient.kt @@ -1,43 +1,27 @@ package net.corda.node.services.network -import com.google.common.util.concurrent.MoreExecutors import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SignedData import net.corda.core.internal.* -import net.corda.core.messaging.DataFeed -import net.corda.core.messaging.ParametersUpdateInfo -import net.corda.core.node.NetworkParameters import net.corda.core.node.NodeInfo import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.utilities.contextLogger -import net.corda.core.utilities.minutes import net.corda.core.utilities.seconds import net.corda.core.utilities.trace -import net.corda.node.services.api.NetworkMapCacheInternal -import net.corda.node.utilities.NamedThreadFactory import net.corda.node.utilities.registration.cacheControl import net.corda.nodeapi.internal.SignedNodeInfo -import net.corda.nodeapi.internal.network.NETWORK_PARAMS_UPDATE_FILE_NAME -import net.corda.nodeapi.internal.network.NetworkMap -import net.corda.nodeapi.internal.network.ParametersUpdate -import net.corda.nodeapi.internal.network.verifiedNetworkMapCert -import rx.Subscription -import rx.subjects.PublishSubject +import net.corda.nodeapi.internal.network.* import java.io.BufferedReader -import java.io.Closeable import java.net.URL -import java.nio.file.Path -import java.nio.file.StandardCopyOption import java.security.cert.X509Certificate import java.time.Duration -import java.util.concurrent.Executors -import java.util.concurrent.TimeUnit class NetworkMapClient(compatibilityZoneURL: URL, val trustedRoot: X509Certificate) { companion object { private val logger = contextLogger() } + private val networkMapUrl = URL("$compatibilityZoneURL/network-map") fun publish(signedNodeInfo: SignedNodeInfo) { @@ -57,10 +41,13 @@ class NetworkMapClient(compatibilityZoneURL: URL, val trustedRoot: X509Certifica fun getNetworkMap(): NetworkMapResponse { logger.trace { "Fetching network map update from $networkMapUrl." } val connection = networkMapUrl.openHttpConnection() - val signedNetworkMap = connection.responseAs>() + val signedNetworkMap = connection.responseAs() val networkMap = signedNetworkMap.verifiedNetworkMapCert(trustedRoot) val timeout = connection.cacheControl().maxAgeSeconds().seconds - logger.trace { "Fetched network map update from $networkMapUrl successfully, retrieved ${networkMap.nodeInfoHashes.size} node info hashes. Node Info hashes: ${networkMap.nodeInfoHashes.joinToString("\n")}" } + logger.trace { + "Fetched network map update from $networkMapUrl successfully, retrieved ${networkMap.nodeInfoHashes.size} " + + "node info hashes. Node Info hashes:\n${networkMap.nodeInfoHashes.joinToString("\n")}" + } return NetworkMapResponse(networkMap, timeout) } @@ -72,10 +59,10 @@ class NetworkMapClient(compatibilityZoneURL: URL, val trustedRoot: X509Certifica return verifiedNodeInfo } - fun getNetworkParameters(networkParameterHash: SecureHash): SignedDataWithCert { + fun getNetworkParameters(networkParameterHash: SecureHash): SignedNetworkParameters { val url = URL("$networkMapUrl/network-parameters/$networkParameterHash") logger.trace { "Fetching network parameters: '$networkParameterHash' from $url." } - val networkParameter = url.openHttpConnection().responseAs>() + val networkParameter = url.openHttpConnection().responseAs() logger.trace { "Fetched network parameters: '$networkParameterHash' successfully. Network Parameters: $networkParameter" } return networkParameter } @@ -89,143 +76,4 @@ class NetworkMapClient(compatibilityZoneURL: URL, val trustedRoot: X509Certifica } } -data class NetworkMapResponse(val networkMap: NetworkMap, val cacheMaxAge: Duration) - -class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, - private val fileWatcher: NodeInfoWatcher, - private val networkMapClient: NetworkMapClient?, - private val currentParametersHash: SecureHash, - private val baseDirectory: Path) : Closeable { - companion object { - private val logger = contextLogger() - private val retryInterval = 1.minutes - } - - private var newNetworkParameters: Pair>? = null - - fun track(): DataFeed { - val currentUpdateInfo = newNetworkParameters?.let { - ParametersUpdateInfo(it.first.newParametersHash, it.second.verified(), it.first.description, it.first.updateDeadline) - } - return DataFeed( - currentUpdateInfo, - parametersUpdatesTrack - ) - } - - private val parametersUpdatesTrack: PublishSubject = PublishSubject.create() - private val executor = Executors.newSingleThreadScheduledExecutor(NamedThreadFactory("Network Map Updater Thread", Executors.defaultThreadFactory())) - private var fileWatcherSubscription: Subscription? = null - - override fun close() { - fileWatcherSubscription?.unsubscribe() - MoreExecutors.shutdownAndAwaitTermination(executor, 50, TimeUnit.SECONDS) - } - - fun updateNodeInfo(newInfo: NodeInfo, signNodeInfo: (NodeInfo) -> SignedNodeInfo) { - val oldInfo = networkMapCache.getNodeByLegalIdentity(newInfo.legalIdentities.first()) - // Compare node info without timestamp. - if (newInfo.copy(serial = 0L) == oldInfo?.copy(serial = 0L)) return - - // Only publish and write to disk if there are changes to the node info. - val signedNodeInfo = signNodeInfo(newInfo) - networkMapCache.addNode(newInfo) - fileWatcher.saveToFile(signedNodeInfo) - - if (networkMapClient != null) { - tryPublishNodeInfoAsync(signedNodeInfo, networkMapClient) - } - } - - fun subscribeToNetworkMap() { - require(fileWatcherSubscription == null) { "Should not call this method twice." } - // Subscribe to file based networkMap - fileWatcherSubscription = fileWatcher.nodeInfoUpdates().subscribe(networkMapCache::addNode) - - if (networkMapClient == null) return - // Subscribe to remote network map if configured. - val task = object : Runnable { - override fun run() { - val nextScheduleDelay = try { - val (networkMap, cacheTimeout) = networkMapClient.getNetworkMap() - networkMap.parametersUpdate?.let { handleUpdateNetworkParameters(it) } - if (currentParametersHash != networkMap.networkParameterHash) { - // TODO This needs special handling (node omitted update process/didn't accept new parameters or didn't restart on updateDeadline) - logger.error("Node is using parameters with hash: $currentParametersHash but network map is advertising: ${networkMap.networkParameterHash}.\n" + - "Please update node to use correct network parameters file.\"") - System.exit(1) - } - val currentNodeHashes = networkMapCache.allNodeHashes - val hashesFromNetworkMap = networkMap.nodeInfoHashes - (hashesFromNetworkMap - currentNodeHashes).mapNotNull { - // Download new node info from network map - try { - networkMapClient.getNodeInfo(it) - } catch (e: Exception) { - // Failure to retrieve one node info shouldn't stop the whole update, log and return null instead. - logger.warn("Error encountered when downloading node info '$it', skipping...", e) - null - } - }.forEach { - // Add new node info to the network map cache, these could be new node info or modification of node info for existing nodes. - networkMapCache.addNode(it) - } - // Remove node info from network map. - (currentNodeHashes - hashesFromNetworkMap - fileWatcher.processedNodeInfoHashes) - .mapNotNull(networkMapCache::getNodeByHash) - .forEach(networkMapCache::removeNode) - cacheTimeout - } catch (t: Throwable) { - logger.warn("Error encountered while updating network map, will retry in ${retryInterval.seconds} seconds", t) - retryInterval - } - // Schedule the next update. - executor.schedule(this, nextScheduleDelay.toMillis(), TimeUnit.MILLISECONDS) - } - } - executor.submit(task) // The check may be expensive, so always run it in the background even the first time. - } - - private fun tryPublishNodeInfoAsync(signedNodeInfo: SignedNodeInfo, networkMapClient: NetworkMapClient) { - val task = object : Runnable { - override fun run() { - try { - networkMapClient.publish(signedNodeInfo) - } catch (t: Throwable) { - logger.warn("Error encountered while publishing node info, will retry in ${retryInterval.seconds} seconds.", t) - // TODO: Exponential backoff? - executor.schedule(this, retryInterval.toMillis(), TimeUnit.MILLISECONDS) - } - } - } - executor.submit(task) - } - - private fun handleUpdateNetworkParameters(update: ParametersUpdate) { - if (update.newParametersHash == newNetworkParameters?.first?.newParametersHash) { // This update was handled already. - return - } - val newParameters = networkMapClient?.getNetworkParameters(update.newParametersHash) - if (newParameters != null) { - logger.info("Downloaded new network parameters: $newParameters from the update: $update") - newNetworkParameters = Pair(update, newParameters) - parametersUpdatesTrack.onNext(ParametersUpdateInfo(update.newParametersHash, newParameters.verifiedNetworkMapCert(networkMapClient!!.trustedRoot), update.description, update.updateDeadline)) - } - } - - fun acceptNewNetworkParameters(parametersHash: SecureHash, sign: (SecureHash) -> SignedData) { - networkMapClient ?: throw IllegalStateException("Network parameters updates are not support without compatibility zone configured") - // TODO This scenario will happen if node was restarted and didn't download parameters yet, but we accepted them. Add persisting of newest parameters from update. - val (_, newParams) = newNetworkParameters ?: throw IllegalArgumentException("Couldn't find parameters update for the hash: $parametersHash") - val newParametersHash = newParams.verifiedNetworkMapCert(networkMapClient.trustedRoot).serialize().hash // We should check that we sign the right data structure hash. - if (parametersHash == newParametersHash) { - // The latest parameters have priority. - newParams.serialize() - .open() - .copyTo(baseDirectory / NETWORK_PARAMS_UPDATE_FILE_NAME, StandardCopyOption.REPLACE_EXISTING) - networkMapClient.ackNetworkParametersUpdate(sign(parametersHash)) - } else { - throw IllegalArgumentException("Refused to accept parameters with hash $parametersHash because network map advertises update with hash $newParametersHash. Please check newest version") - } - } -} \ No newline at end of file +data class NetworkMapResponse(val payload: NetworkMap, val cacheMaxAge: Duration) diff --git a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt new file mode 100644 index 0000000000..414f0f15c3 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt @@ -0,0 +1,178 @@ +package net.corda.node.services.network + +import com.google.common.util.concurrent.MoreExecutors +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.SignedData +import net.corda.core.internal.copyTo +import net.corda.core.internal.div +import net.corda.core.messaging.DataFeed +import net.corda.core.messaging.ParametersUpdateInfo +import net.corda.core.node.NodeInfo +import net.corda.core.serialization.serialize +import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.minutes +import net.corda.node.services.api.NetworkMapCacheInternal +import net.corda.node.utilities.NamedThreadFactory +import net.corda.nodeapi.internal.SignedNodeInfo +import net.corda.nodeapi.internal.network.NETWORK_PARAMS_UPDATE_FILE_NAME +import net.corda.nodeapi.internal.network.ParametersUpdate +import net.corda.nodeapi.internal.network.SignedNetworkParameters +import net.corda.nodeapi.internal.network.verifiedNetworkMapCert +import rx.Subscription +import rx.subjects.PublishSubject +import java.nio.file.Path +import java.nio.file.StandardCopyOption +import java.time.Duration +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + +class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, + private val fileWatcher: NodeInfoWatcher, + private val networkMapClient: NetworkMapClient?, + private val currentParametersHash: SecureHash, + private val baseDirectory: Path +) : AutoCloseable { + companion object { + private val logger = contextLogger() + private val defaultRetryInterval = 1.minutes + } + + private val parametersUpdatesTrack: PublishSubject = PublishSubject.create() + private val executor = Executors.newSingleThreadScheduledExecutor(NamedThreadFactory("Network Map Updater Thread", Executors.defaultThreadFactory())) + private var newNetworkParameters: Pair? = null + private var fileWatcherSubscription: Subscription? = null + + override fun close() { + fileWatcherSubscription?.unsubscribe() + MoreExecutors.shutdownAndAwaitTermination(executor, 50, TimeUnit.SECONDS) + } + + fun trackParametersUpdate(): DataFeed { + val currentUpdateInfo = newNetworkParameters?.let { + ParametersUpdateInfo(it.first.newParametersHash, it.second.verified(), it.first.description, it.first.updateDeadline) + } + return DataFeed(currentUpdateInfo, parametersUpdatesTrack) + } + + fun updateNodeInfo(newInfo: NodeInfo, signer: (NodeInfo) -> SignedNodeInfo) { + val oldInfo = networkMapCache.getNodeByLegalIdentity(newInfo.legalIdentities.first()) + // Compare node info without timestamp. + if (newInfo.copy(serial = 0L) == oldInfo?.copy(serial = 0L)) return + + // Only publish and write to disk if there are changes to the node info. + val signedNodeInfo = signer(newInfo) + networkMapCache.addNode(newInfo) + fileWatcher.saveToFile(signedNodeInfo) + + if (networkMapClient != null) { + tryPublishNodeInfoAsync(signedNodeInfo, networkMapClient) + } + } + + private fun tryPublishNodeInfoAsync(signedNodeInfo: SignedNodeInfo, networkMapClient: NetworkMapClient) { + executor.submit(object : Runnable { + override fun run() { + try { + networkMapClient.publish(signedNodeInfo) + } catch (t: Throwable) { + logger.warn("Error encountered while publishing node info, will retry in $defaultRetryInterval", t) + // TODO: Exponential backoff? + executor.schedule(this, defaultRetryInterval.toMillis(), TimeUnit.MILLISECONDS) + } + } + }) + } + + fun subscribeToNetworkMap() { + require(fileWatcherSubscription == null) { "Should not call this method twice." } + // Subscribe to file based networkMap + fileWatcherSubscription = fileWatcher.nodeInfoUpdates().subscribe(networkMapCache::addNode) + + if (networkMapClient == null) return + + // Subscribe to remote network map if configured. + executor.submit(object : Runnable { + override fun run() { + val nextScheduleDelay = try { + updateNetworkMapCache(networkMapClient) + } catch (t: Throwable) { + logger.warn("Error encountered while updating network map, will retry in $defaultRetryInterval", t) + defaultRetryInterval + } + // Schedule the next update. + executor.schedule(this, nextScheduleDelay.toMillis(), TimeUnit.MILLISECONDS) + } + }) // The check may be expensive, so always run it in the background even the first time. + } + + private fun updateNetworkMapCache(networkMapClient: NetworkMapClient): Duration { + val (networkMap, cacheTimeout) = networkMapClient.getNetworkMap() + networkMap.parametersUpdate?.let { handleUpdateNetworkParameters(networkMapClient, it) } + + if (currentParametersHash != networkMap.networkParameterHash) { + // TODO This needs special handling (node omitted update process/didn't accept new parameters or didn't restart on updateDeadline) + logger.error("Node is using parameters with hash: $currentParametersHash but network map is " + + "advertising: ${networkMap.networkParameterHash}.\n" + + "Please update node to use correct network parameters file.\"") + System.exit(1) + } + + val currentNodeHashes = networkMapCache.allNodeHashes + val hashesFromNetworkMap = networkMap.nodeInfoHashes + (hashesFromNetworkMap - currentNodeHashes).mapNotNull { + // Download new node info from network map + try { + networkMapClient.getNodeInfo(it) + } catch (e: Exception) { + // Failure to retrieve one node info shouldn't stop the whole update, log and return null instead. + logger.warn("Error encountered when downloading node info '$it', skipping...", e) + null + } + }.forEach { + // Add new node info to the network map cache, these could be new node info or modification of node info for existing nodes. + networkMapCache.addNode(it) + } + + // Remove node info from network map. + (currentNodeHashes - hashesFromNetworkMap - fileWatcher.processedNodeInfoHashes) + .mapNotNull(networkMapCache::getNodeByHash) + .forEach(networkMapCache::removeNode) + + return cacheTimeout + } + + private fun handleUpdateNetworkParameters(networkMapClient: NetworkMapClient, update: ParametersUpdate) { + if (update.newParametersHash == newNetworkParameters?.first?.newParametersHash) { + // This update was handled already. + return + } + val newParameters = networkMapClient.getNetworkParameters(update.newParametersHash) + logger.info("Downloaded new network parameters: $newParameters from the update: $update") + newNetworkParameters = Pair(update, newParameters) + val updateInfo = ParametersUpdateInfo( + update.newParametersHash, + newParameters.verifiedNetworkMapCert(networkMapClient.trustedRoot), + update.description, + update.updateDeadline) + parametersUpdatesTrack.onNext(updateInfo) + } + + fun acceptNewNetworkParameters(parametersHash: SecureHash, sign: (SecureHash) -> SignedData) { + networkMapClient ?: throw IllegalStateException("Network parameters updates are not support without compatibility zone configured") + // TODO This scenario will happen if node was restarted and didn't download parameters yet, but we accepted them. + // Add persisting of newest parameters from update. + val (_, newParams) = requireNotNull(newNetworkParameters) { "Couldn't find parameters update for the hash: $parametersHash" } + // We should check that we sign the right data structure hash. + val newParametersHash = newParams.verifiedNetworkMapCert(networkMapClient.trustedRoot).serialize().hash + if (parametersHash == newParametersHash) { + // The latest parameters have priority. + newParams.serialize() + .open() + .copyTo(baseDirectory / NETWORK_PARAMS_UPDATE_FILE_NAME, StandardCopyOption.REPLACE_EXISTING) + networkMapClient.ackNetworkParametersUpdate(sign(parametersHash)) + } else { + throw IllegalArgumentException("Refused to accept parameters with hash $parametersHash because network map " + + "advertises update with hash $newParametersHash. Please check newest version") + } + } +} diff --git a/node/src/main/kotlin/net/corda/node/services/network/NodeInfoWatcher.kt b/node/src/main/kotlin/net/corda/node/services/network/NodeInfoWatcher.kt index 3031349d84..aa55fb2552 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/NodeInfoWatcher.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/NodeInfoWatcher.kt @@ -4,12 +4,11 @@ import net.corda.cordform.CordformNode import net.corda.core.crypto.SecureHash import net.corda.core.internal.* import net.corda.core.node.NodeInfo -import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.utilities.contextLogger import net.corda.core.utilities.seconds -import net.corda.nodeapi.internal.network.NodeInfoFilesCopier import net.corda.nodeapi.internal.SignedNodeInfo +import net.corda.nodeapi.internal.network.NodeInfoFilesCopier import rx.Observable import rx.Scheduler import java.io.IOException @@ -118,7 +117,7 @@ class NodeInfoWatcher(private val nodePath: Path, private fun processFile(file: Path): NodeInfo? { return try { logger.info("Reading NodeInfo from file: $file") - val signedData = file.readAll().deserialize() + val signedData = file.readObject() signedData.verified() } catch (e: Exception) { logger.warn("Exception parsing NodeInfo from file. $file", e) diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt index 4c646b8f46..19fb9fd875 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt @@ -7,12 +7,11 @@ import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate -import net.corda.core.node.NotaryInfo import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.internal.concurrent.openFuture -import net.corda.node.internal.schemas.NodeInfoSchemaV1 import net.corda.core.messaging.DataFeed import net.corda.core.node.NodeInfo +import net.corda.core.node.NotaryInfo import net.corda.core.node.services.IdentityService import net.corda.core.node.services.NetworkMapCache.MapChange import net.corda.core.node.services.PartyInfo @@ -21,6 +20,7 @@ import net.corda.core.serialization.serialize import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger import net.corda.core.utilities.loggerFor +import net.corda.node.internal.schemas.NodeInfoSchemaV1 import net.corda.node.services.api.NetworkMapCacheBaseInternal import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.utilities.NonInvalidatingCache @@ -153,8 +153,17 @@ open class PersistentNetworkMapCache( return null } - override fun getNodeByLegalName(name: CordaX500Name): NodeInfo? = getNodesByLegalName(name).firstOrNull() + override fun getNodeByLegalName(name: CordaX500Name): NodeInfo? { + val nodeInfos = getNodesByLegalName(name) + return when (nodeInfos.size) { + 0 -> null + 1 -> nodeInfos[0] + else -> throw IllegalArgumentException("More than one node found with legal name $name") + } + } + override fun getNodesByLegalName(name: CordaX500Name): List = database.transaction { queryByLegalName(session, name) } + override fun getNodesByLegalIdentityKey(identityKey: PublicKey): List = nodesByKeyCache[identityKey] private val nodesByKeyCache = NonInvalidatingCache>(1024, 8, { key -> database.transaction { queryByIdentityKey(session, key) } }) diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt index 784499509d..cd9f7d37f5 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt @@ -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? = 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>( + private val attachmentContentCache = NonInvalidatingWeightBasedCache>>( maxWeight = attachmentContentCacheSize, concurrencyLevel = defaultCordaCacheConcurrencyLevel, - weigher = object : Weigher> { - override fun weigh(key: SecureHash, value: Optional): Int { - return key.size + if (value.isPresent) value.get().size else 0 + weigher = object : Weigher>> { + override fun weigh(key: SecureHash, value: Optional>): 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? { 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>( 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(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 { @@ -328,5 +330,4 @@ class NodeAttachmentService( return results.map { AttachmentId.parse(it.attId) } } - } diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManagerImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManagerImpl.kt index b59a40b86f..1ac69e1a59 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManagerImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManagerImpl.kt @@ -211,6 +211,7 @@ class StateMachineManagerImpl( liveFibers.await() checkpointCheckerThread?.let { MoreExecutors.shutdownAndAwaitTermination(it, 5, SECONDS) } check(!unrestorableCheckpoints) { "Unrestorable checkpoints where created, please check the logs for details." } + scheduler.shutdown() } /** diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt index 979e2d2ef0..a04ce94d6f 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt @@ -12,13 +12,19 @@ import net.corda.core.flows.NotaryError import net.corda.core.flows.NotaryException import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party +import net.corda.core.flows.NotarisationPayload +import net.corda.core.flows.NotarisationRequest import net.corda.core.node.services.NotaryService import net.corda.core.node.services.UniquenessProvider import net.corda.core.schemas.PersistentStateRef import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize +import net.corda.core.transactions.CoreTransaction import net.corda.core.transactions.FilteredTransaction -import net.corda.core.utilities.* +import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.debug +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.unwrap import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.config.BFTSMaRtConfiguration import net.corda.node.utilities.AppendOnlyPersistentMap @@ -67,25 +73,25 @@ class BFTNonValidatingNotaryService( replicaHolder.getOrThrow() // It's enough to wait for the ServiceReplica constructor to return. } - fun commitTransaction(tx: Any, otherSide: Party) = client.commitTransaction(tx, otherSide) + fun commitTransaction(payload: NotarisationPayload, otherSide: Party) = client.commitTransaction(payload, otherSide) override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic = ServiceFlow(otherPartySession, this) private class ServiceFlow(val otherSideSession: FlowSession, val service: BFTNonValidatingNotaryService) : FlowLogic() { @Suspendable override fun call(): Void? { - val stx = otherSideSession.receive().unwrap { it } - val signatures = commit(stx) + val payload = otherSideSession.receive().unwrap { it } + val signatures = commit(payload) otherSideSession.send(signatures) return null } - private fun commit(stx: FilteredTransaction): List { - val response = service.commitTransaction(stx, otherSideSession.counterparty) + private fun commit(payload: NotarisationPayload): List { + val response = service.commitTransaction(payload, otherSideSession.counterparty) when (response) { is BFTSMaRt.ClusterResponse.Error -> throw NotaryException(response.error) is BFTSMaRt.ClusterResponse.Signatures -> { - log.debug("All input states of transaction ${stx.id} have been committed") + log.debug("All input states of transaction ${payload.coreTransaction.id} have been committed") return response.txSignatures } } @@ -132,28 +138,34 @@ class BFTNonValidatingNotaryService( notaryIdentityKey: PublicKey) : BFTSMaRt.Replica(config, replicaId, createMap, services, notaryIdentityKey) { override fun executeCommand(command: ByteArray): ByteArray { - val request = command.deserialize() - val ftx = request.tx as FilteredTransaction - val response = verifyAndCommitTx(ftx, request.callerIdentity) + val commitRequest = command.deserialize() + verifyRequest(commitRequest) + val response = verifyAndCommitTx(commitRequest.payload.coreTransaction, commitRequest.callerIdentity) return response.serialize().bytes } - fun verifyAndCommitTx(ftx: FilteredTransaction, callerIdentity: Party): BFTSMaRt.ReplicaResponse { + private fun verifyAndCommitTx(transaction: CoreTransaction, callerIdentity: Party): BFTSMaRt.ReplicaResponse { return try { - val id = ftx.id - val inputs = ftx.inputs - val notary = ftx.notary - NotaryService.validateTimeWindow(services.clock, ftx.timeWindow) + val id = transaction.id + val inputs = transaction.inputs + val notary = transaction.notary + if (transaction is FilteredTransaction) NotaryService.validateTimeWindow(services.clock, transaction.timeWindow) if (notary !in services.myInfo.legalIdentities) throw NotaryException(NotaryError.WrongNotary) commitInputStates(inputs, id, callerIdentity) log.debug { "Inputs committed successfully, signing $id" } - BFTSMaRt.ReplicaResponse.Signature(sign(ftx)) + BFTSMaRt.ReplicaResponse.Signature(sign(id)) } catch (e: NotaryException) { log.debug { "Error processing transaction: ${e.error}" } BFTSMaRt.ReplicaResponse.Error(e.error) } } + private fun verifyRequest(commitRequest: BFTSMaRt.CommitRequest) { + val transaction = commitRequest.payload.coreTransaction + val notarisationRequest = NotarisationRequest(transaction.inputs, transaction.id) + notarisationRequest.verifySignature(commitRequest.payload.requestSignature, commitRequest.callerIdentity) + // TODO: persist the signature for traceability. + } } override fun start() { diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt b/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt index 84ca283b48..899ecde450 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt @@ -17,6 +17,7 @@ import net.corda.core.crypto.* import net.corda.core.flows.NotaryError import net.corda.core.flows.NotaryException import net.corda.core.identity.Party +import net.corda.core.flows.NotarisationPayload import net.corda.core.internal.declaredField import net.corda.core.internal.toTypedArray import net.corda.core.node.services.UniquenessProvider @@ -25,8 +26,6 @@ import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize -import net.corda.core.transactions.FilteredTransaction -import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug import net.corda.node.services.api.ServiceHubInternal @@ -52,7 +51,7 @@ import java.util.* object BFTSMaRt { /** Sent from [Client] to [Replica]. */ @CordaSerializable - data class CommitRequest(val tx: Any, val callerIdentity: Party) + data class CommitRequest(val payload: NotarisationPayload, val callerIdentity: Party) /** Sent from [Replica] to [Client]. */ @CordaSerializable @@ -101,13 +100,12 @@ object BFTSMaRt { * Sends a transaction commit request to the BFT cluster. The [proxy] will deliver the request to every * replica, and block until a sufficient number of replies are received. */ - fun commitTransaction(transaction: Any, otherSide: Party): ClusterResponse { - require(transaction is FilteredTransaction || transaction is SignedTransaction) { "Unsupported transaction type: ${transaction.javaClass.name}" } + fun commitTransaction(payload: NotarisationPayload, otherSide: Party): ClusterResponse { awaitClientConnectionToCluster() cluster.waitUntilAllReplicasHaveInitialized() - val requestBytes = CommitRequest(transaction, otherSide).serialize().bytes + val requestBytes = CommitRequest(payload, otherSide).serialize().bytes val responseBytes = proxy.invokeOrdered(requestBytes) - return responseBytes.deserialize() + return responseBytes.deserialize() } /** A comparator to check if replies from two replicas are the same. */ @@ -242,12 +240,15 @@ object BFTSMaRt { } } + /** Generates a signature over an arbitrary array of bytes. */ protected fun sign(bytes: ByteArray): DigitalSignature.WithKey { return services.database.transaction { services.keyManagementService.sign(bytes, notaryIdentityKey) } } - protected fun sign(filteredTransaction: FilteredTransaction): TransactionSignature { - return services.database.transaction { services.createSignature(filteredTransaction, notaryIdentityKey) } + /** Generates a transaction signature over the specified transaction [txId]. */ + protected fun sign(txId: SecureHash): TransactionSignature { + val signableData = SignableData(txId, SignatureMetadata(services.myInfo.platformVersion, Crypto.findSignatureScheme(notaryIdentityKey).schemeNumberID)) + return services.keyManagementService.sign(signableData, notaryIdentityKey) } // TODO: diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt b/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt index a12d565d52..5eaaa71c52 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt @@ -1,11 +1,15 @@ package net.corda.node.services.transactions import co.paralleluniverse.fibers.Suspendable -import net.corda.core.flows.FlowSession import net.corda.core.contracts.ComponentGroupEnum +import net.corda.core.flows.FlowSession import net.corda.core.flows.NotaryFlow import net.corda.core.flows.TransactionParts +import net.corda.core.flows.NotarisationPayload +import net.corda.core.flows.NotarisationRequest +import net.corda.core.internal.validateRequest import net.corda.core.node.services.TrustedAuthorityNotaryService +import net.corda.core.transactions.CoreTransaction import net.corda.core.transactions.FilteredTransaction import net.corda.core.transactions.NotaryChangeWireTransaction import net.corda.core.utilities.unwrap @@ -21,22 +25,30 @@ class NonValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAut */ @Suspendable override fun receiveAndVerifyTx(): TransactionParts { - val parts = otherSideSession.receive().unwrap { - when (it) { - is FilteredTransaction -> { - it.verify() - it.checkAllComponentsVisible(ComponentGroupEnum.INPUTS_GROUP) - it.checkAllComponentsVisible(ComponentGroupEnum.TIMEWINDOW_GROUP) - val notary = it.notary - TransactionParts(it.id, it.inputs, it.timeWindow, notary) - } - is NotaryChangeWireTransaction -> TransactionParts(it.id, it.inputs, null, it.notary) - else -> { - throw IllegalArgumentException("Received unexpected transaction type: ${it::class.java.simpleName}," + - "expected either ${FilteredTransaction::class.java.simpleName} or ${NotaryChangeWireTransaction::class.java.simpleName}") + return otherSideSession.receive().unwrap { payload -> + val transaction = payload.coreTransaction + val request = NotarisationRequest(transaction.inputs, transaction.id) + validateRequest(request, payload.requestSignature) + extractParts(transaction) + } + } + + private fun extractParts(tx: CoreTransaction): TransactionParts { + return when (tx) { + is FilteredTransaction -> { + tx.apply { + verify() + checkAllComponentsVisible(ComponentGroupEnum.INPUTS_GROUP) + checkAllComponentsVisible(ComponentGroupEnum.TIMEWINDOW_GROUP) } + val notary = tx.notary + TransactionParts(tx.id, tx.inputs, tx.timeWindow, notary) + } + is NotaryChangeWireTransaction -> TransactionParts(tx.id, tx.inputs, null, tx.notary) + else -> { + throw IllegalArgumentException("Received unexpected transaction type: ${tx::class.java.simpleName}," + + "expected either ${FilteredTransaction::class.java.simpleName} or ${NotaryChangeWireTransaction::class.java.simpleName}") } } - return parts } } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt index 8ce7ba6365..907428429c 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt @@ -4,8 +4,14 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.TimeWindow import net.corda.core.contracts.TransactionVerificationException import net.corda.core.flows.* +import net.corda.core.flows.NotarisationPayload +import net.corda.core.flows.NotarisationRequest +import net.corda.core.internal.ResolveTransactionsFlow +import net.corda.core.internal.validateRequest import net.corda.core.node.services.TrustedAuthorityNotaryService +import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionWithSignatures +import net.corda.core.utilities.unwrap import java.security.SignatureException /** @@ -22,15 +28,15 @@ class ValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthor @Suspendable override fun receiveAndVerifyTx(): TransactionParts { try { - val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false)) + val stx = receiveTransaction() val notary = stx.notary checkNotary(notary) val timeWindow: TimeWindow? = if (stx.isNotaryChangeTransaction()) null else stx.tx.timeWindow - val transactionWithSignatures = stx.resolveTransactionWithSignatures(serviceHub) - checkSignatures(transactionWithSignatures) + resolveAndContractVerify(stx) + verifySignatures(stx) return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!) } catch (e: Exception) { throw when (e) { @@ -41,6 +47,26 @@ class ValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthor } } + @Suspendable + private fun receiveTransaction(): SignedTransaction { + return otherSideSession.receive().unwrap { + val stx = it.signedTransaction + validateRequest(NotarisationRequest(stx.inputs, stx.id), it.requestSignature) + stx + } + } + + @Suspendable + private fun resolveAndContractVerify(stx: SignedTransaction) { + subFlow(ResolveTransactionsFlow(stx, otherSideSession)) + stx.verify(serviceHub, false) + } + + private fun verifySignatures(stx: SignedTransaction) { + val transactionWithSignatures = stx.resolveTransactionWithSignatures(serviceHub) + checkSignatures(transactionWithSignatures) + } + private fun checkSignatures(tx: TransactionWithSignatures) { try { tx.verifySignaturesExcept(service.notaryIdentityKey) diff --git a/node/src/main/kotlin/net/corda/node/utilities/DemoClock.kt b/node/src/main/kotlin/net/corda/node/utilities/DemoClock.kt index 0d6ab6b48c..ac2e82e962 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/DemoClock.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/DemoClock.kt @@ -1,7 +1,7 @@ package net.corda.node.utilities import net.corda.core.internal.until -import net.corda.node.internal.MutableClock +import net.corda.node.MutableClock import java.time.Clock import java.time.LocalDate import javax.annotation.concurrent.ThreadSafe diff --git a/node/src/main/kotlin/net/corda/node/utilities/JVMAgentRegistry.kt b/node/src/main/kotlin/net/corda/node/utilities/JVMAgentRegistry.kt new file mode 100644 index 0000000000..54850dee10 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/utilities/JVMAgentRegistry.kt @@ -0,0 +1,50 @@ +package net.corda.node.utilities + +import com.ea.agentloader.AgentLoader +import net.corda.core.internal.exists +import net.corda.core.internal.isRegularFile +import java.net.URLClassLoader +import java.nio.file.Path +import java.nio.file.Paths +import java.util.concurrent.ConcurrentHashMap + +/** + * Helper class for loading JVM agents dynamically + */ +object JVMAgentRegistry { + + /** + * Names and options of loaded agents + */ + val loadedAgents = ConcurrentHashMap() + + /** + * Load and attach agent located at given [jar], unless [loadedAgents] + * indicate that one of its instance has been already loaded. + */ + fun attach(agentName: String, options: String, jar: Path) { + loadedAgents.computeIfAbsent(agentName.toLowerCase()) { + AgentLoader.loadAgent(jar.toString(), options) + options + } + } + + /** + * Attempt finding location of jar for given agent by first searching into + * "drivers" directory of [nodeBaseDirectory] and then falling back to + * classpath. Returns null if no match is found. + */ + fun resolveAgentJar(jarFileName: String, driversDir: Path): Path? { + require(jarFileName.endsWith(".jar")) { "jarFileName does not have .jar suffix" } + + val path = Paths.get(driversDir.toString(), jarFileName) + return if (path.exists() && path.isRegularFile()) { + path + } else { + (this::class.java.classLoader as? URLClassLoader) + ?.urLs + ?.map { Paths.get(it.path) } + ?.firstOrNull { it.fileName.toString() == jarFileName } + } + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/utilities/NodeBuildProperties.kt b/node/src/main/kotlin/net/corda/node/utilities/NodeBuildProperties.kt new file mode 100644 index 0000000000..5d7d96cf56 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/utilities/NodeBuildProperties.kt @@ -0,0 +1,27 @@ +package net.corda.node.utilities + +import java.util.* + +/** + * Expose properties defined in top-level 'constants.properties' file. + */ +object NodeBuildProperties { + + // Note: initialization order is important + private val data by lazy { + Properties().apply { + NodeBuildProperties::class.java.getResourceAsStream("/build.properties") + ?.let { load(it) } + } + } + + /** + * Jolokia dependency version + */ + val JOLOKIA_AGENT_VERSION = get("jolokiaAgentVersion") + + /** + * Get property value by name + */ + fun get(key: String): String? = data.getProperty(key) +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt index 287f4b41c9..1a253a701a 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt @@ -3,7 +3,10 @@ package net.corda.node.utilities.registration import net.corda.core.crypto.Crypto import net.corda.core.identity.CordaX500Name import net.corda.core.internal.* +import net.corda.node.NodeRegistrationOption import net.corda.node.services.config.NodeConfiguration +import net.corda.nodeapi.internal.DevIdentityGenerator +import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509KeyStore import net.corda.nodeapi.internal.crypto.X509Utilities @@ -22,10 +25,18 @@ import java.security.cert.X509Certificate * Helper for managing the node registration process, which checks for any existing certificates and requests them if * needed. */ -class NetworkRegistrationHelper(private val config: NodeConfiguration, +class NetworkRegistrationHelper(private val config: SSLConfiguration, + private val myLegalName: CordaX500Name, + private val emailAddress: String, private val certService: NetworkRegistrationService, - networkRootTrustStorePath: Path, - networkRootTruststorePassword: String) { + private val networkRootTrustStorePath: Path, + networkRootTrustStorePassword: String, + private val certRole: CertRole) { + + // Constructor for corda node, cert role is restricted to [CertRole.NODE_CA]. + constructor(config: NodeConfiguration, certService: NetworkRegistrationService, regConfig: NodeRegistrationOption) : + this(config, config.myLegalName, config.emailAddress, certService, regConfig.networkRootTrustStorePath, regConfig.networkRootTrustStorePassword, CertRole.NODE_CA) + private companion object { const val SELF_SIGNED_PRIVATE_KEY = "Self Signed Private Key" } @@ -41,7 +52,7 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, "$networkRootTrustStorePath does not exist. This file must contain the root CA cert of your compatibility zone. " + "Please contact your CZ operator." } - rootTrustStore = X509KeyStore.fromFile(networkRootTrustStorePath, networkRootTruststorePassword) + rootTrustStore = X509KeyStore.fromFile(networkRootTrustStorePath, networkRootTrustStorePassword) rootCert = rootTrustStore.getCertificate(CORDA_ROOT_CA) } @@ -68,7 +79,7 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, // We use the self sign certificate to store the key temporarily in the keystore while waiting for the request approval. if (SELF_SIGNED_PRIVATE_KEY !in nodeKeyStore) { val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val selfSignCert = X509Utilities.createSelfSignedCACertificate(config.myLegalName.x500Principal, keyPair) + val selfSignCert = X509Utilities.createSelfSignedCACertificate(myLegalName.x500Principal, keyPair) // Save to the key store. nodeKeyStore.setPrivateKey(SELF_SIGNED_PRIVATE_KEY, keyPair.private, listOf(selfSignCert), keyPassword = privateKeyPassword) nodeKeyStore.save() @@ -87,36 +98,59 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, throw certificateRequestException } - val nodeCaCert = certificates[0] + val certificate = certificates.first() val nodeCaSubject = try { - CordaX500Name.build(nodeCaCert.subjectX500Principal) + CordaX500Name.build(certificate.subjectX500Principal) } catch (e: IllegalArgumentException) { throw CertificateRequestException("Received node CA cert has invalid subject name: ${e.message}") } - if (nodeCaSubject != config.myLegalName) { + if (nodeCaSubject != myLegalName) { throw CertificateRequestException("Subject of received node CA cert doesn't match with node legal name: $nodeCaSubject") } val nodeCaCertRole = try { - CertRole.extract(nodeCaCert) + CertRole.extract(certificate) } catch (e: IllegalArgumentException) { throw CertificateRequestException("Unable to extract cert role from received node CA cert: ${e.message}") } - if (nodeCaCertRole != CertRole.NODE_CA) { - throw CertificateRequestException("Received node CA cert has invalid role: $nodeCaCertRole") - } // Validate certificate chain returned from the doorman with the root cert obtained via out-of-band process, to prevent MITM attack on doorman server. X509Utilities.validateCertificateChain(rootCert, certificates) println("Certificate signing request approved, storing private key with the certificate chain.") - // Save private key and certificate chain to the key store. - nodeKeyStore.setPrivateKey(CORDA_CLIENT_CA, keyPair.private, certificates, keyPassword = privateKeyPassword) - nodeKeyStore.internal.deleteEntry(SELF_SIGNED_PRIVATE_KEY) - nodeKeyStore.save() - println("Node private key and certificate stored in ${config.nodeKeystore}.") + when (nodeCaCertRole) { + CertRole.NODE_CA -> { + // Save private key and certificate chain to the key store. + nodeKeyStore.setPrivateKey(CORDA_CLIENT_CA, keyPair.private, certificates, keyPassword = privateKeyPassword) + nodeKeyStore.internal.deleteEntry(SELF_SIGNED_PRIVATE_KEY) + nodeKeyStore.save() + println("Node private key and certificate stored in ${config.nodeKeystore}.") + + config.loadSslKeyStore(createNew = true).update { + println("Generating SSL certificate for node messaging service.") + val sslKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val sslCert = X509Utilities.createCertificate( + CertificateType.TLS, + certificate, + keyPair, + myLegalName.x500Principal, + sslKeyPair.public) + setPrivateKey(CORDA_CLIENT_TLS, sslKeyPair.private, listOf(sslCert) + certificates) + } + println("SSL private key and certificate stored in ${config.sslKeystore}.") + } + // TODO: Fix this, this is not needed in corda node. + CertRole.SERVICE_IDENTITY -> { + // Only create keystore containing notary's key for service identity role. + nodeKeyStore.setPrivateKey("${DevIdentityGenerator.DISTRIBUTED_NOTARY_ALIAS_PREFIX}-private-key", keyPair.private, certificates, keyPassword = privateKeyPassword) + nodeKeyStore.internal.deleteEntry(SELF_SIGNED_PRIVATE_KEY) + nodeKeyStore.save() + println("Service identity private key and certificate stored in ${config.nodeKeystore}.") + } + else -> throw CertificateRequestException("Received node CA cert has invalid role: $nodeCaCertRole") + } // Save root certificates to trust store. config.loadTrustStore(createNew = true).update { println("Generating trust store for corda node.") @@ -124,20 +158,6 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, setCertificate(CORDA_ROOT_CA, certificates.last()) } println("Node trust store stored in ${config.trustStoreFile}.") - - config.loadSslKeyStore(createNew = true).update { - println("Generating SSL certificate for node messaging service.") - val sslKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val sslCert = X509Utilities.createCertificate( - CertificateType.TLS, - nodeCaCert, - keyPair, - config.myLegalName.x500Principal, - sslKeyPair.public) - setPrivateKey(CORDA_CLIENT_TLS, sslKeyPair.private, listOf(sslCert) + certificates) - } - println("SSL private key and certificate stored in ${config.sslKeystore}.") - // All done, clean up temp files. requestIdStore.deleteIfExists() } @@ -169,15 +189,15 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, private fun submitOrResumeCertificateSigningRequest(keyPair: KeyPair): String { // Retrieve request id from file if exists, else post a request to server. return if (!requestIdStore.exists()) { - val request = X509Utilities.createCertificateSigningRequest(config.myLegalName.x500Principal, config.emailAddress, keyPair) + val request = X509Utilities.createCertificateSigningRequest(myLegalName.x500Principal, emailAddress, keyPair, certRole) val writer = StringWriter() JcaPEMWriter(writer).use { it.writeObject(PemObject("CERTIFICATE REQUEST", request.encoded)) } println("Certificate signing request with the following information will be submitted to the Corda certificate signing server.") println() - println("Legal Name: ${config.myLegalName}") - println("Email: ${config.emailAddress}") + println("Legal Name: $myLegalName") + println("Email: $emailAddress") println() println("Public Key: ${keyPair.public}") println() diff --git a/node/src/main/resources/build.properties b/node/src/main/resources/build.properties new file mode 100644 index 0000000000..72577915e7 --- /dev/null +++ b/node/src/main/resources/build.properties @@ -0,0 +1,5 @@ +# Build constants exported as resource file to make them visible in Node program +# Note: sadly, due to present limitation of IntelliJ-IDEA in processing resource files, these constants cannot be +# imported from top-level 'constants.properties' file + +jolokiaAgentVersion=1.3.7 \ No newline at end of file diff --git a/node/src/main/resources/reference.conf b/node/src/main/resources/reference.conf index f0e0c7deec..c4f7fe169a 100644 --- a/node/src/main/resources/reference.conf +++ b/node/src/main/resources/reference.conf @@ -1,6 +1,5 @@ myLegalName = "Vast Global MegaCorp, Ltd" emailAddress = "admin@company.com" -exportJMXto = "http" keyStorePassword = "cordacadevpass" trustStorePassword = "trustpass" dataSourceProperties = { diff --git a/node/src/test/kotlin/net/corda/node/ArgsParserTest.kt b/node/src/test/kotlin/net/corda/node/ArgsParserTest.kt index 96f98d8c33..8ddb9707e9 100644 --- a/node/src/test/kotlin/net/corda/node/ArgsParserTest.kt +++ b/node/src/test/kotlin/net/corda/node/ArgsParserTest.kt @@ -2,12 +2,14 @@ package net.corda.node import joptsimple.OptionException import net.corda.core.internal.div +import net.corda.nodeapi.internal.crypto.X509KeyStore import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.junit.Test import org.slf4j.event.Level import java.nio.file.Paths import kotlin.test.assertEquals +import kotlin.test.assertNotNull class ArgsParserTest { private val parser = ArgsParser() @@ -21,14 +23,12 @@ class ArgsParserTest { help = false, logToConsole = false, loggingLevel = Level.INFO, - isRegistration = false, + nodeRegistrationConfig = null, isVersion = false, noLocalShell = false, sshdServer = false, justGenerateNodeInfo = false, - bootstrapRaftCluster = false, - networkRootTruststorePassword = null, - networkRootTruststorePath = null)) + bootstrapRaftCluster = false)) } @Test @@ -113,11 +113,17 @@ class ArgsParserTest { @Test fun `initial-registration`() { - val truststorePath = Paths.get("truststore") / "file.jks" + val truststorePath = workingDirectory / "truststore" / "file.jks" + assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy { + parser.parse("--initial-registration", "--network-root-truststore", "$truststorePath", "--network-root-truststore-password", "password-test") + }.withMessageContaining("Network root trust store path").withMessageContaining("doesn't exist") + + X509KeyStore.fromFile(truststorePath, "dummy_password", createNew = true) + val cmdLineOptions = parser.parse("--initial-registration", "--network-root-truststore", "$truststorePath", "--network-root-truststore-password", "password-test") - assertThat(cmdLineOptions.isRegistration).isTrue() - assertEquals(truststorePath.toAbsolutePath(), cmdLineOptions.networkRootTruststorePath) - assertEquals("password-test", cmdLineOptions.networkRootTruststorePassword) + assertNotNull(cmdLineOptions.nodeRegistrationConfig) + assertEquals(truststorePath.toAbsolutePath(), cmdLineOptions.nodeRegistrationConfig?.networkRootTrustStorePath) + assertEquals("password-test", cmdLineOptions.nodeRegistrationConfig?.networkRootTrustStorePassword) } @Test diff --git a/node/src/test/kotlin/net/corda/node/internal/AbstractNodeTests.kt b/node/src/test/kotlin/net/corda/node/internal/AbstractNodeTests.kt index 5dc2f28844..c7494b6f2d 100644 --- a/node/src/test/kotlin/net/corda/node/internal/AbstractNodeTests.kt +++ b/node/src/test/kotlin/net/corda/node/internal/AbstractNodeTests.kt @@ -42,12 +42,22 @@ class AbstractNodeTests { @Test fun `H2 fix is applied`() { - repeat(if (relaxedThoroughness) 1 else 100) { - // Two "nodes" seems to be the magic number to reproduce the problem: - val urls = (0 until 2).map { freshURL() } + val pool = Executors.newFixedThreadPool(5) + val runs = if (relaxedThoroughness) 1 else 100 + (0 until runs).map { + // Four "nodes" seems to be the magic number to reproduce the problem on CI: + val urls = (0 until 4).map { freshURL() } // Haven't been able to reproduce in a warm JVM: - assertEquals(0, startJavaProcess(urls).waitFor()) - } + pool.fork { + assertEquals(0, startJavaProcess(urls).waitFor()) + } + }.transpose().getOrThrow() + pool.shutdown() + // The above will always run all processes, even if the very first fails + // In theory this can be handled better, but + // a) we expect to run all the runs, that's how the test passes + // b) it would require relatively complex handling (futures+threads), which is not worth it + // because of a) } } diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt index 8526b36bcd..ebfb3795b2 100644 --- a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt +++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt @@ -1,18 +1,35 @@ package net.corda.node.internal.cordapp +import com.typesafe.config.Config +import com.typesafe.config.ConfigFactory +import junit.framework.Assert.assertNull +import net.corda.core.internal.cordapp.CordappConfigProvider import net.corda.core.node.services.AttachmentStorage +import net.corda.testing.common.internal.testNetworkParameters +import net.corda.testing.internal.MockCordappConfigProvider import net.corda.testing.services.MockAttachmentStorage -import org.junit.Assert +import org.assertj.core.api.Assertions.assertThat +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull import org.junit.Before import org.junit.Test +import java.net.URL class CordappProviderImplTests { - companion object { - private val isolatedJAR = this::class.java.getResource("isolated.jar")!! - private val emptyJAR = this::class.java.getResource("empty.jar")!! + private companion object { + val isolatedJAR = this::class.java.getResource("isolated.jar")!! + // TODO: Cordapp name should differ from the JAR name + val isolatedCordappName = "isolated" + val emptyJAR = this::class.java.getResource("empty.jar")!! + val validConfig = ConfigFactory.parseString("key=value") + + val stubConfigProvider = object : CordappConfigProvider { + override fun getConfigByName(name: String): Config = ConfigFactory.empty() + } } private lateinit var attachmentStore: AttachmentStorage + private val whitelistedContractImplementations = testNetworkParameters().whitelistedContractImplementations @Before fun setup() { @@ -21,43 +38,56 @@ 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) + } + + @Test + fun `test cordapp configuration`() { + val configProvider = MockCordappConfigProvider() + configProvider.cordappConfigs.put(isolatedCordappName, validConfig) + val loader = CordappLoader.createDevMode(listOf(isolatedJAR)) + val provider = CordappProviderImpl(loader, configProvider, attachmentStore, whitelistedContractImplementations) + + val expected = provider.getAppContext(provider.cordapps.first()).config + + assertThat(expected.getString("key")).isEqualTo("value") + } + + private fun newCordappProvider(vararg urls: URL): CordappProviderImpl { + val loader = CordappLoader.createDevMode(urls.toList()) + return CordappProviderImpl(loader, stubConfigProvider, attachmentStore, whitelistedContractImplementations) } } diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/TypesafeCordappConfigTests.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/TypesafeCordappConfigTests.kt new file mode 100644 index 0000000000..6a966ea4b0 --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/TypesafeCordappConfigTests.kt @@ -0,0 +1,47 @@ +package net.corda.node.internal.cordapp + +import com.typesafe.config.ConfigFactory +import net.corda.core.cordapp.CordappConfigException +import org.junit.Test +import org.assertj.core.api.Assertions.assertThat + +class TypesafeCordappConfigTests { + @Test + fun `test that all value types can be retrieved`() { + val config = ConfigFactory.parseString("string=string\nint=1\nfloat=1.0\ndouble=1.0\nnumber=2\ndouble=1.01\nbool=false") + val cordappConf = TypesafeCordappConfig(config) + + assertThat(cordappConf.get("string")).isEqualTo("string") + assertThat(cordappConf.getString("string")).isEqualTo("string") + assertThat(cordappConf.getInt("int")).isEqualTo(1) + assertThat(cordappConf.getFloat("float")).isEqualTo(1.0F) + assertThat(cordappConf.getDouble("double")).isEqualTo(1.01) + assertThat(cordappConf.getNumber("number")).isEqualTo(2) + assertThat(cordappConf.getBoolean("bool")).isEqualTo(false) + } + + @Test + fun `test a nested path`() { + val config = ConfigFactory.parseString("outer: { inner: string }") + val cordappConf = TypesafeCordappConfig(config) + + assertThat(cordappConf.getString("outer.inner")).isEqualTo("string") + } + + @Test + fun `test exists determines existence and lack of existence correctly`() { + val config = ConfigFactory.parseString("exists=exists") + val cordappConf = TypesafeCordappConfig(config) + + assertThat(cordappConf.exists("exists")).isTrue() + assertThat(cordappConf.exists("notexists")).isFalse() + } + + @Test(expected = CordappConfigException::class) + fun `test that an exception is thrown when trying to access a non-extant field`() { + val config = ConfigFactory.empty() + val cordappConf = TypesafeCordappConfig(config) + + cordappConf.get("anything") + } +} \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index 6ffcdcd158..5395c0453d 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -103,7 +103,7 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { // We run this in parallel threads to help catch any race conditions that may exist. The other tests // we run in the unit test thread exclusively to speed things up, ensure deterministic results and // allow interruption half way through. - mockNet = InternalMockNetwork(threadPerNode = true, cordappPackages = cordappPackages) + mockNet = InternalMockNetwork(cordappPackages = cordappPackages, threadPerNode = true) val ledgerIdentityService = rigorousMock() MockServices(cordappPackages, MEGA_CORP.name, ledgerIdentityService).ledger(DUMMY_NOTARY) { val notaryNode = mockNet.defaultNotaryNode @@ -155,7 +155,7 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { @Test(expected = InsufficientBalanceException::class) fun `trade cash for commercial paper fails using soft locking`() { - mockNet = InternalMockNetwork(threadPerNode = true, cordappPackages = cordappPackages) + mockNet = InternalMockNetwork(cordappPackages = cordappPackages, threadPerNode = true) val ledgerIdentityService = rigorousMock() MockServices(cordappPackages, MEGA_CORP.name, ledgerIdentityService).ledger(DUMMY_NOTARY) { val notaryNode = mockNet.defaultNotaryNode diff --git a/node/src/test/kotlin/net/corda/node/modes/draining/ScheduledFlowsDrainingModeTest.kt b/node/src/test/kotlin/net/corda/node/modes/draining/ScheduledFlowsDrainingModeTest.kt index 9e85b92863..eed40b383d 100644 --- a/node/src/test/kotlin/net/corda/node/modes/draining/ScheduledFlowsDrainingModeTest.kt +++ b/node/src/test/kotlin/net/corda/node/modes/draining/ScheduledFlowsDrainingModeTest.kt @@ -52,7 +52,7 @@ class ScheduledFlowsDrainingModeTest { @Before fun setup() { - mockNet = InternalMockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.testing.contracts")) + mockNet = InternalMockNetwork(cordappPackages = listOf("net.corda.testing.contracts"), threadPerNode = true) aliceNode = mockNet.createNode(MockNodeParameters(legalName = ALICE_NAME)) bobNode = mockNet.createNode(MockNodeParameters(legalName = BOB_NAME)) notary = mockNet.defaultNotaryIdentity diff --git a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt index cc1559cf5b..96a3b63a87 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt @@ -103,7 +103,7 @@ class ScheduledFlowTests { @Before fun setup() { - mockNet = InternalMockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.testing.contracts")) + mockNet = InternalMockNetwork(cordappPackages = listOf("net.corda.testing.contracts"), threadPerNode = true) aliceNode = mockNet.createNode(MockNodeParameters(legalName = ALICE_NAME)) bobNode = mockNet.createNode(MockNodeParameters(legalName = BOB_NAME)) notary = mockNet.defaultNotaryIdentity diff --git a/node/src/test/kotlin/net/corda/node/services/identity/InMemoryIdentityServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/identity/InMemoryIdentityServiceTests.kt index f1256db27d..ff11860082 100644 --- a/node/src/test/kotlin/net/corda/node/services/identity/InMemoryIdentityServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/identity/InMemoryIdentityServiceTests.kt @@ -11,6 +11,8 @@ import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.crypto.x509Certificates import net.corda.testing.core.* +import net.corda.testing.internal.DEV_INTERMEDIATE_CA +import net.corda.testing.internal.DEV_ROOT_CA import org.junit.Rule import org.junit.Test import kotlin.test.assertEquals diff --git a/node/src/test/kotlin/net/corda/node/services/identity/PersistentIdentityServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/identity/PersistentIdentityServiceTests.kt index dda6516173..6ac282ef88 100644 --- a/node/src/test/kotlin/net/corda/node/services/identity/PersistentIdentityServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/identity/PersistentIdentityServiceTests.kt @@ -15,6 +15,8 @@ import net.corda.nodeapi.internal.crypto.x509Certificates import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.core.* +import net.corda.testing.internal.DEV_INTERMEDIATE_CA +import net.corda.testing.internal.DEV_ROOT_CA import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import net.corda.testing.node.makeTestIdentityService import org.junit.After diff --git a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt index 2f6dfc23a1..d9e9f5512e 100644 --- a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt @@ -67,7 +67,7 @@ class ArtemisMessagingTest { doReturn("trustpass").whenever(it).trustStorePassword doReturn("cordacadevpass").whenever(it).keyStorePassword doReturn(NetworkHostAndPort("0.0.0.0", serverPort)).whenever(it).p2pAddress - doReturn("").whenever(it).exportJMXto + doReturn(null).whenever(it).jmxMonitoringHttpPort doReturn(emptyList()).whenever(it).certificateChainCheckPolicies doReturn(5).whenever(it).messageRedeliveryDelaySeconds } diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt index e8809bb833..03bb1a1667 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt @@ -6,9 +6,9 @@ import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.testing.core.getTestPartyAndCertificate -import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNodeParameters import net.corda.testing.core.singleIdentity +import net.corda.testing.node.internal.InternalMockNetwork import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Test @@ -18,7 +18,7 @@ import kotlin.test.assertNotNull import kotlin.test.assertNull class NetworkMapCacheTest { - private val mockNet = MockNetwork(emptyList()) + private val mockNet = InternalMockNetwork(emptyList()) @After fun teardown() { diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapClientTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapClientTest.kt index 1edd35db01..c888ed6818 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapClientTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapClientTest.kt @@ -7,10 +7,10 @@ import net.corda.core.serialization.serialize import net.corda.core.utilities.seconds import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME -import net.corda.testing.core.DEV_ROOT_CA import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.driver.PortAllocation +import net.corda.testing.internal.DEV_ROOT_CA import net.corda.testing.internal.TestNodeInfoBuilder import net.corda.testing.internal.createNodeInfoAndSigned import net.corda.testing.internal.signWith @@ -57,7 +57,7 @@ class NetworkMapClientTest { val nodeInfoHash = nodeInfo.serialize().sha256() - assertThat(networkMapClient.getNetworkMap().networkMap.nodeInfoHashes).containsExactly(nodeInfoHash) + assertThat(networkMapClient.getNetworkMap().payload.nodeInfoHashes).containsExactly(nodeInfoHash) assertEquals(nodeInfo, networkMapClient.getNodeInfo(nodeInfoHash)) val (nodeInfo2, signedNodeInfo2) = createNodeInfoAndSigned(BOB_NAME) @@ -65,7 +65,7 @@ class NetworkMapClientTest { networkMapClient.publish(signedNodeInfo2) val nodeInfoHash2 = nodeInfo2.serialize().sha256() - assertThat(networkMapClient.getNetworkMap().networkMap.nodeInfoHashes).containsExactly(nodeInfoHash, nodeInfoHash2) + assertThat(networkMapClient.getNetworkMap().payload.nodeInfoHashes).containsExactly(nodeInfoHash, nodeInfoHash2) assertEquals(cacheTimeout, networkMapClient.getNetworkMap().cacheMaxAge) assertEquals(nodeInfo2, networkMapClient.getNodeInfo(nodeInfoHash2)) } @@ -98,7 +98,7 @@ class NetworkMapClientTest { @Test fun `handle parameters update`() { - val nextParameters = testNetworkParameters(emptyList(), epoch = 2) + val nextParameters = testNetworkParameters(epoch = 2) val originalNetworkParameterHash = server.networkParameters.serialize().hash val nextNetworkParameterHash = nextParameters.serialize().hash val description = "Test parameters" diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt index 8609520c73..0647f46253 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt @@ -15,19 +15,16 @@ import net.corda.core.internal.* import net.corda.core.messaging.ParametersUpdateInfo import net.corda.core.node.NetworkParameters import net.corda.core.node.NodeInfo -import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.utilities.millis import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.createDevNetworkMapCa import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair -import net.corda.nodeapi.internal.network.NETWORK_PARAMS_UPDATE_FILE_NAME -import net.corda.nodeapi.internal.network.NetworkMap -import net.corda.nodeapi.internal.network.ParametersUpdate -import net.corda.nodeapi.internal.network.verifiedNetworkMapCert +import net.corda.nodeapi.internal.network.* import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.* +import net.corda.testing.internal.DEV_ROOT_CA import net.corda.testing.internal.TestNodeInfoBuilder import net.corda.testing.internal.createNodeInfoAndSigned import org.assertj.core.api.Assertions.assertThat @@ -196,11 +193,11 @@ class NetworkMapUpdaterTest { @Test fun `emit new parameters update info on parameters update from network map`() { - val paramsFeed = updater.track() + val paramsFeed = updater.trackParametersUpdate() val snapshot = paramsFeed.snapshot val updates = paramsFeed.updates.bufferUntilSubscribed() assertEquals(null, snapshot) - val newParameters = testNetworkParameters(emptyList(), epoch = 2) + val newParameters = testNetworkParameters(epoch = 2) val updateDeadline = Instant.now().plus(1, ChronoUnit.DAYS) scheduleParametersUpdate(newParameters, "Test update", updateDeadline) updater.subscribeToNetworkMap() @@ -218,7 +215,7 @@ class NetworkMapUpdaterTest { @Test fun `ack network parameters update`() { - val newParameters = testNetworkParameters(emptyList(), epoch = 314) + val newParameters = testNetworkParameters(epoch = 314) scheduleParametersUpdate(newParameters, "Test update", Instant.MIN) updater.subscribeToNetworkMap() // TODO: Remove sleep in unit test. @@ -228,7 +225,7 @@ class NetworkMapUpdaterTest { updater.acceptNewNetworkParameters(newHash, { hash -> hash.serialize().sign(keyPair)}) verify(networkMapClient).ackNetworkParametersUpdate(any()) val updateFile = baseDir / NETWORK_PARAMS_UPDATE_FILE_NAME - val signedNetworkParams = updateFile.readAll().deserialize>() + val signedNetworkParams = updateFile.readObject() val paramsFromFile = signedNetworkParams.verifiedNetworkMapCert(DEV_ROOT_CA.certificate) assertEquals(newParameters, paramsFromFile) } diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkParametersReaderTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkParametersReaderTest.kt index b6b9e107cd..370657e42c 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NetworkParametersReaderTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkParametersReaderTest.kt @@ -2,19 +2,17 @@ package net.corda.node.services.network import com.google.common.jimfs.Configuration import com.google.common.jimfs.Jimfs -import net.corda.core.internal.* -import net.corda.core.node.NetworkParameters -import net.corda.core.serialization.deserialize +import net.corda.core.internal.createDirectories +import net.corda.core.internal.div +import net.corda.core.internal.exists +import net.corda.core.internal.readObject import net.corda.core.utilities.seconds import net.corda.node.internal.NetworkParametersReader -import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME -import net.corda.nodeapi.internal.network.NETWORK_PARAMS_UPDATE_FILE_NAME -import net.corda.nodeapi.internal.network.NetworkParametersCopier -import net.corda.nodeapi.internal.network.verifiedNetworkMapCert +import net.corda.nodeapi.internal.network.* import net.corda.testing.common.internal.testNetworkParameters -import net.corda.testing.core.DEV_ROOT_CA import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.driver.PortAllocation +import net.corda.testing.internal.DEV_ROOT_CA import net.corda.testing.node.internal.network.NetworkMapServer import org.junit.After import org.junit.Before @@ -50,14 +48,16 @@ class NetworkParametersReaderTest { fun `read correct set of parameters from file`() { val fs = Jimfs.newFileSystem(Configuration.unix()) val baseDirectory = fs.getPath("/node").createDirectories() - val oldParameters = testNetworkParameters(emptyList(), epoch = 1) + val oldParameters = testNetworkParameters(epoch = 1) NetworkParametersCopier(oldParameters).install(baseDirectory) NetworkParametersCopier(server.networkParameters, update = true).install(baseDirectory) // Parameters update file. val parameters = NetworkParametersReader(DEV_ROOT_CA.certificate, networkMapClient, baseDirectory).networkParameters assertFalse((baseDirectory / NETWORK_PARAMS_UPDATE_FILE_NAME).exists()) assertEquals(server.networkParameters, parameters) // Parameters from update should be moved to `network-parameters` file. - val parametersFromFile = (baseDirectory / NETWORK_PARAMS_FILE_NAME).readAll().deserialize>().verifiedNetworkMapCert(DEV_ROOT_CA.certificate) + val parametersFromFile = (baseDirectory / NETWORK_PARAMS_FILE_NAME) + .readObject() + .verifiedNetworkMapCert(DEV_ROOT_CA.certificate) assertEquals(server.networkParameters, parametersFromFile) } } \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt index 3c6e6d3b39..aa31cf7dd8 100644 --- a/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt @@ -14,6 +14,7 @@ import net.corda.node.services.schema.NodeSchemaService.NodeNotaryV1 import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.InProcess import net.corda.testing.driver.driver +import net.corda.testing.driver.internal.InProcessImpl import net.corda.testing.internal.vault.DummyLinearStateSchemaV1 import net.corda.testing.node.MockNetwork import org.hibernate.annotations.Cascade @@ -80,7 +81,7 @@ class NodeSchemaServiceTest { fun `custom schemas are loaded eagerly`() { val expected = setOf("PARENTS", "CHILDREN") val tables = driver(DriverParameters(startNodesInProcess = true)) { - (defaultNotaryNode.getOrThrow() as InProcess).database.transaction { + (defaultNotaryNode.getOrThrow() as InProcessImpl).database.transaction { session.createNativeQuery("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES").list() } } diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt index da80ff4b2e..9797fca399 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt @@ -77,8 +77,8 @@ class FlowFrameworkTests { @Before fun start() { mockNet = InternalMockNetwork( - servicePeerAllocationStrategy = RoundRobin(), - cordappPackages = listOf("net.corda.finance.contracts", "net.corda.testing.contracts") + cordappPackages = listOf("net.corda.finance.contracts", "net.corda.testing.contracts"), + servicePeerAllocationStrategy = RoundRobin() ) aliceNode = mockNet.createNode(MockNodeParameters(legalName = ALICE_NAME)) bobNode = mockNet.createNode(MockNodeParameters(legalName = BOB_NAME)) diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/MaxTransactionSizeTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/MaxTransactionSizeTests.kt index 7b6a077b8d..29db80ab9d 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/MaxTransactionSizeTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/MaxTransactionSizeTests.kt @@ -8,6 +8,7 @@ import net.corda.core.internal.InputStreamAndHash import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.getOrThrow import net.corda.node.services.api.StartedNodeServices +import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyState import net.corda.testing.core.ALICE_NAME @@ -20,6 +21,7 @@ import net.corda.testing.node.startFlow import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Before +import org.junit.Ignore import org.junit.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -34,7 +36,7 @@ class MaxTransactionSizeTests { @Before fun setup() { - mockNet = MockNetwork(listOf("net.corda.testing.contracts", "net.corda.node.services.transactions"), maxTransactionSize = 3_000_000) + mockNet = MockNetwork(listOf("net.corda.testing.contracts", "net.corda.node.services.transactions"), networkParameters = testNetworkParameters(maxTransactionSize = 3_000_000)) val aliceNode = mockNet.createNode(MockNodeParameters(legalName = ALICE_NAME)) val bobNode = mockNet.createNode(MockNodeParameters(legalName = BOB_NAME)) notaryServices = mockNet.defaultNotaryNode.services diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt index 5cf3319fbb..d37c5a3259 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt @@ -3,24 +3,35 @@ package net.corda.node.services.transactions import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature +import net.corda.core.crypto.sign import net.corda.core.flows.NotaryError import net.corda.core.flows.NotaryException import net.corda.core.flows.NotaryFlow import net.corda.core.identity.Party +import net.corda.core.flows.NotarisationPayload +import net.corda.core.flows.NotarisationRequest +import net.corda.core.flows.NotarisationRequestSignature +import net.corda.core.internal.generateSignature +import net.corda.core.messaging.MessageRecipients import net.corda.core.node.ServiceHub +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.serialize import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.seconds import net.corda.node.services.api.StartedNodeServices -import net.corda.testing.core.ALICE_NAME +import net.corda.node.services.messaging.Message +import net.corda.node.services.statemachine.InitialSessionMessage import net.corda.testing.contracts.DummyContract +import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.dummyCommand -import net.corda.testing.node.MockNetwork -import net.corda.testing.node.MockNodeParameters import net.corda.testing.core.singleIdentity -import net.corda.testing.node.startFlow +import net.corda.testing.node.* import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Before @@ -34,6 +45,7 @@ import kotlin.test.assertTrue class NotaryServiceTests { private lateinit var mockNet: MockNetwork private lateinit var notaryServices: StartedNodeServices + private lateinit var aliceNode: StartedMockNode private lateinit var aliceServices: StartedNodeServices private lateinit var notary: Party private lateinit var alice: Party @@ -41,7 +53,8 @@ class NotaryServiceTests { @Before fun setup() { mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts")) - aliceServices = mockNet.createNode(MockNodeParameters(legalName = ALICE_NAME)).services + aliceNode = mockNet.createNode(MockNodeParameters(legalName = ALICE_NAME)) + aliceServices = aliceNode.services notaryServices = mockNet.defaultNotaryNode.services //TODO get rid of that notary = mockNet.defaultNotaryIdentity alice = aliceServices.myInfo.singleIdentity() @@ -159,6 +172,70 @@ class NotaryServiceTests { notaryError.conflict.verified() } + @Test + fun `should reject when notarisation request not signed by the requesting party`() { + runNotarisationAndInterceptClientPayload { originalPayload -> + val transaction = originalPayload.signedTransaction + val randomKeyPair = Crypto.generateKeyPair() + val bytesToSign = NotarisationRequest(transaction.inputs, transaction.id).serialize().bytes + val modifiedSignature = NotarisationRequestSignature(randomKeyPair.sign(bytesToSign), aliceServices.myInfo.platformVersion) + originalPayload.copy(requestSignature = modifiedSignature) + } + } + + @Test + fun `should reject when incorrect notarisation request signed - inputs don't match`() { + runNotarisationAndInterceptClientPayload { originalPayload -> + val transaction = originalPayload.signedTransaction + val wrongInputs = listOf(StateRef(SecureHash.randomSHA256(), 0)) + val request = NotarisationRequest(wrongInputs, transaction.id) + val modifiedSignature = request.generateSignature(aliceServices) + originalPayload.copy(requestSignature = modifiedSignature) + } + } + + @Test + fun `should reject when incorrect notarisation request signed - transaction id doesn't match`() { + runNotarisationAndInterceptClientPayload { originalPayload -> + val transaction = originalPayload.signedTransaction + val wrongTransactionId = SecureHash.randomSHA256() + val request = NotarisationRequest(transaction.inputs, wrongTransactionId) + val modifiedSignature = request.generateSignature(aliceServices) + originalPayload.copy(requestSignature = modifiedSignature) + } + } + + private fun runNotarisationAndInterceptClientPayload(payloadModifier: (NotarisationPayload) -> NotarisationPayload) { + aliceNode.setMessagingServiceSpy(object : MessagingServiceSpy(aliceNode.network) { + override fun send(message: Message, target: MessageRecipients, retryId: Long?, sequenceKey: Any, additionalHeaders: Map) { + val messageData = message.data.deserialize() as? InitialSessionMessage + val payload = messageData?.firstPayload!!.deserialize() + + if (payload is NotarisationPayload) { + val alteredPayload = payloadModifier(payload) + val alteredMessageData = messageData.copy(firstPayload = alteredPayload.serialize()) + val alteredMessage = InMemoryMessagingNetwork.InMemoryMessage(message.topic, OpaqueBytes(alteredMessageData.serialize().bytes), message.uniqueMessageId) + messagingService.send(alteredMessage, target, retryId) + + } else { + messagingService.send(message, target, retryId) + } + } + }) + + val stx = run { + val inputState = issueState(aliceServices, alice) + val tx = TransactionBuilder(notary) + .addInputState(inputState) + .addCommand(dummyCommand(alice.owningKey)) + aliceServices.signInitialTransaction(tx) + } + + val future = runNotaryClient(stx) + val ex = assertFailsWith(NotaryException::class) { future.getOrThrow() } + assertThat(ex.error).isInstanceOf(NotaryError.RequestSignatureInvalid::class.java) + } + private fun runNotaryClient(stx: SignedTransaction): CordaFuture> { val flow = NotaryFlow.Client(stx) val future = aliceServices.startFlow(flow) diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index c75bf3f447..04e0b34c37 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -110,7 +110,7 @@ class VaultQueryTests { cordappPackages, makeTestIdentityService(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, dummyCashIssuer.identity, dummyNotary.identity), megaCorp, - DUMMY_NOTARY_KEY) + moreKeys = DUMMY_NOTARY_KEY) database = databaseAndServices.first services = databaseAndServices.second vaultFiller = VaultFiller(services, dummyNotary) diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt index 02e5e1c1dd..c3b423cfe1 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt @@ -27,7 +27,6 @@ import net.corda.node.internal.InitiatedFlowFactory import net.corda.node.services.api.VaultServiceInternal import net.corda.nodeapi.internal.persistence.HibernateConfiguration import net.corda.testing.core.chooseIdentity -import net.corda.testing.node.MockNetwork import net.corda.testing.internal.rigorousMock import net.corda.testing.node.MockNodeParameters import net.corda.testing.node.internal.InternalMockNetwork diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt index 1dcb7cb984..e7e3a50ea2 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt @@ -1,6 +1,9 @@ package net.corda.node.services.vault -import net.corda.core.contracts.* +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.InsufficientBalanceException +import net.corda.core.contracts.LinearState +import net.corda.core.contracts.UniqueIdentifier import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.AnonymousParty import net.corda.core.identity.CordaX500Name @@ -74,7 +77,7 @@ class VaultWithCashTest { cordappPackages, makeTestIdentityService(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, dummyCashIssuer.identity, dummyNotary.identity), TestIdentity(MEGA_CORP.name, servicesKey), - dummyNotary.keyPair) + moreKeys = dummyNotary.keyPair) database = databaseAndServices.first services = databaseAndServices.second vaultFiller = VaultFiller(services, dummyNotary) diff --git a/node/src/test/kotlin/net/corda/node/utilities/ClockUtilsTest.kt b/node/src/test/kotlin/net/corda/node/utilities/ClockUtilsTest.kt index d8dbfac65f..571dee482b 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/ClockUtilsTest.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/ClockUtilsTest.kt @@ -8,8 +8,8 @@ import com.google.common.util.concurrent.SettableFuture import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.hours import net.corda.core.utilities.minutes -import net.corda.node.internal.CordaClock -import net.corda.node.internal.SimpleClock +import net.corda.node.CordaClock +import net.corda.node.SimpleClock import net.corda.node.services.events.NodeSchedulerService import net.corda.testing.node.TestClock import org.junit.After diff --git a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt index 2371ab4437..d77e61bcae 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt @@ -13,7 +13,9 @@ import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.internal.x500Name import net.corda.core.utilities.seconds +import net.corda.node.NodeRegistrationOption import net.corda.node.services.config.NodeConfiguration +import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509KeyStore import net.corda.nodeapi.internal.crypto.X509Utilities @@ -141,6 +143,38 @@ class NetworkRegistrationHelperTest { }.isInstanceOf(CertPathValidatorException::class.java) } + @Test + fun `create service identity cert`() { + assertThat(config.nodeKeystore).doesNotExist() + assertThat(config.sslKeystore).doesNotExist() + assertThat(config.trustStoreFile).doesNotExist() + + val serviceIdentityCertPath = createServiceIdentityCertPath() + + saveNetworkTrustStore(serviceIdentityCertPath.last()) + createRegistrationHelper(serviceIdentityCertPath).buildKeystore() + + val nodeKeystore = config.loadNodeKeyStore() + val trustStore = config.loadTrustStore() + assertThat(config.sslKeystore).doesNotExist() + + val serviceIdentityAlias = "${DevIdentityGenerator.DISTRIBUTED_NOTARY_ALIAS_PREFIX}-private-key" + + nodeKeystore.run { + assertFalse(contains(X509Utilities.CORDA_INTERMEDIATE_CA)) + assertFalse(contains(X509Utilities.CORDA_ROOT_CA)) + assertFalse(contains(X509Utilities.CORDA_CLIENT_TLS)) + assertFalse(contains(X509Utilities.CORDA_CLIENT_CA)) + assertThat(getCertificateChain(serviceIdentityAlias)).containsExactlyElementsOf(serviceIdentityCertPath) + } + + trustStore.run { + assertFalse(contains(X509Utilities.CORDA_CLIENT_CA)) + assertFalse(contains(X509Utilities.CORDA_INTERMEDIATE_CA)) + assertThat(getCertificate(X509Utilities.CORDA_ROOT_CA)).isEqualTo(serviceIdentityCertPath.last()) + } + } + private fun createNodeCaCertPath(type: CertificateType = CertificateType.NODE_CA, legalName: CordaX500Name = nodeLegalName): List { val (rootCa, intermediateCa) = createDevIntermediateCaCertPath() @@ -156,12 +190,25 @@ class NetworkRegistrationHelperTest { return listOf(nodeCaCert, intermediateCa.certificate, rootCa.certificate) } + private fun createServiceIdentityCertPath(type: CertificateType = CertificateType.SERVICE_IDENTITY, + legalName: CordaX500Name = nodeLegalName): List { + val (rootCa, intermediateCa) = createDevIntermediateCaCertPath() + val keyPair = Crypto.generateKeyPair() + val serviceIdentityCert = X509Utilities.createCertificate( + type, + intermediateCa.certificate, + intermediateCa.keyPair, + legalName.x500Principal, + keyPair.public) + return listOf(serviceIdentityCert, intermediateCa.certificate, rootCa.certificate) + } + private fun createRegistrationHelper(response: List): NetworkRegistrationHelper { val certService = rigorousMock().also { doReturn(requestId).whenever(it).submitRequest(any()) doReturn(CertificateResponse(5.seconds, response)).whenever(it).retrieveCertificates(eq(requestId)) } - return NetworkRegistrationHelper(config, certService, config.certificatesDirectory / networkRootTrustStoreFileName, networkRootTrustStorePassword) + return NetworkRegistrationHelper(config, certService, NodeRegistrationOption(config.certificatesDirectory / networkRootTrustStoreFileName, networkRootTrustStorePassword)) } private fun saveNetworkTrustStore(rootCert: X509Certificate) { diff --git a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaCordformTest.kt b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaCordformTest.kt index 34432277a1..e5235553b6 100644 --- a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaCordformTest.kt +++ b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaCordformTest.kt @@ -2,13 +2,13 @@ package net.corda.bank import net.corda.finance.DOLLARS import net.corda.finance.POUNDS -import net.corda.testing.node.internal.demorun.deployNodesThen +import net.corda.testing.node.internal.demorun.nodeRunner import org.junit.Test class BankOfCordaCordformTest { @Test fun `run demo`() { - BankOfCordaCordform().deployNodesThen { + BankOfCordaCordform().nodeRunner().scanPackages(listOf("net.corda.finance")).deployAndRunNodesThen { IssueCash.requestWebIssue(30000.POUNDS) IssueCash.requestRpcIssue(20000.DOLLARS) } diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaCordform.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaCordform.kt index 260d37365f..e750504a8f 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaCordform.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaCordform.kt @@ -25,8 +25,8 @@ private const val BOC_RPC_ADMIN_PORT = 10015 private const val BOC_WEB_PORT = 10007 class BankOfCordaCordform : CordformDefinition() { + // TODO: Readd finance dependency - will fail without it init { - cordappPackages += "net.corda.finance" node { name(NOTARY_NAME) notary(NotaryConfig(validating = true)) @@ -65,7 +65,7 @@ class BankOfCordaCordform : CordformDefinition() { object DeployNodes { @JvmStatic fun main(args: Array) { - BankOfCordaCordform().deployNodes() + BankOfCordaCordform().nodeRunner().scanPackages(listOf("net.corda.finance")).deployAndRunNodes() } } diff --git a/samples/cordapp-configuration/README.md b/samples/cordapp-configuration/README.md new file mode 100644 index 0000000000..651e8debc9 --- /dev/null +++ b/samples/cordapp-configuration/README.md @@ -0,0 +1,23 @@ +# Cordapp Configuration Sample + +This sample shows a simple example of how to use per-cordapp configuration. It includes; + +* A configuration file +* Gradle build file to show how to install your Cordapp configuration +* A flow that consumes the Cordapp configuration + +## Usage + +To run the sample you must first build it from the project root with; + + ./gradlew deployNodes + +This will deploy the node with the configuration installed. +The relevant section is the ``deployNodes`` task. + +## Running + +* Windows: `build\nodes\runnodes` +* Mac/Linux: `./build/nodes/runnodes` + +Once the nodes have started up and show a prompt you can now run your flow. \ No newline at end of file diff --git a/samples/cordapp-configuration/build.gradle b/samples/cordapp-configuration/build.gradle new file mode 100644 index 0000000000..37edc09147 --- /dev/null +++ b/samples/cordapp-configuration/build.gradle @@ -0,0 +1,54 @@ +apply plugin: 'kotlin' +apply plugin: 'java' +apply plugin: 'net.corda.plugins.cordapp' +apply plugin: 'net.corda.plugins.cordformation' + +dependencies { + cordaCompile project(":core") + cordaCompile project(":node-api") + cordaCompile project(path: ":node:capsule", configuration: 'runtimeArtifacts') + cordaCompile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') +} + +task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { + ext.rpcUsers = [['username': "default", 'password': "default", 'permissions': [ 'ALL' ]]] + + directory "./build/nodes" + node { + name "O=Notary Service,L=Zurich,C=CH" + notary = [validating : true] + p2pPort 10002 + rpcSettings { + port 10003 + adminPort 10004 + } + } + node { + name "O=Bank A,L=London,C=GB" + p2pPort 10005 + cordapps = [] + rpcUsers = ext.rpcUsers + // This configures the default cordapp for this node + projectCordapp { + config "someStringValue=test" + } + rpcSettings { + port 10007 + adminPort 10008 + } + } + node { + name "O=Bank B,L=New York,C=US" + p2pPort 10009 + cordapps = [] + rpcUsers = ext.rpcUsers + // This configures the default cordapp for this node + projectCordapp { + config project.file("src/config.conf") + } + rpcSettings { + port 10011 + adminPort 10012 + } + } +} \ No newline at end of file diff --git a/samples/cordapp-configuration/src/config.conf b/samples/cordapp-configuration/src/config.conf new file mode 100644 index 0000000000..5e2d9fdcd6 --- /dev/null +++ b/samples/cordapp-configuration/src/config.conf @@ -0,0 +1,5 @@ +someStringValue=hello world +someIntValue=1 +nested: { + value: a string +} \ No newline at end of file diff --git a/samples/cordapp-configuration/src/main/kotlin/net/corda/configsample/ConfigSampleFlow.kt b/samples/cordapp-configuration/src/main/kotlin/net/corda/configsample/ConfigSampleFlow.kt new file mode 100644 index 0000000000..251830f538 --- /dev/null +++ b/samples/cordapp-configuration/src/main/kotlin/net/corda/configsample/ConfigSampleFlow.kt @@ -0,0 +1,10 @@ +package net.corda.configsample + +import net.corda.core.flows.FlowLogic + +class ConfigSampleFlow : FlowLogic() { + override fun call(): String { + val config = serviceHub.getAppContext().config + return config.getString("someStringValue") + } +} \ No newline at end of file diff --git a/samples/irs-demo/cordapp/build.gradle b/samples/irs-demo/cordapp/build.gradle index 00cf16c846..5ee75f4ee2 100644 --- a/samples/irs-demo/cordapp/build.gradle +++ b/samples/irs-demo/cordapp/build.gradle @@ -95,10 +95,10 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { } node { name "O=Regulator,L=Moscow,C=RU" - p2pPort 10010 + p2pPort 10011 rpcSettings { - port 10009 - adminPort 10029 + port 10012 + adminPort 10032 } cordapps = ["${project.group}:finance:$corda_release_version"] cordapps = ["${project(":finance").group}:finance:$corda_release_version"] diff --git a/samples/irs-demo/src/system-test/kotlin/IRSDemoDockerTest.kt b/samples/irs-demo/src/system-test/kotlin/IRSDemoDockerTest.kt index 034d4ff2d4..b40e829c9f 100644 --- a/samples/irs-demo/src/system-test/kotlin/IRSDemoDockerTest.kt +++ b/samples/irs-demo/src/system-test/kotlin/IRSDemoDockerTest.kt @@ -19,16 +19,16 @@ import kotlin.test.assertTrue class IRSDemoDockerTest { companion object { - private fun ensureSystemVariable(variable: String) { - if (System.getProperty(variable) == null) { - throw IllegalStateException("System variable $variable not set. Please refer to README file for proper setup instructions.") + private fun ensureProperty(property: String) { + if (System.getProperty(property) == null) { + throw IllegalStateException("System property $property not set. Please refer to README file for proper setup instructions.") } } init { - ensureSystemVariable("CORDAPP_DOCKER_COMPOSE") - ensureSystemVariable("WEB_DOCKER_COMPOSE") - ensureSystemVariable("phantomjs.binary.path") + ensureProperty("CORDAPP_DOCKER_COMPOSE") + ensureProperty("WEB_DOCKER_COMPOSE") + ensureProperty("phantomjs.binary.path") } @ClassRule diff --git a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt index 66ba4e7cbd..4b0a828e9f 100644 --- a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt +++ b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt @@ -71,9 +71,9 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, } val mockNet = InternalMockNetwork( + cordappPackages = listOf("net.corda.finance.contract", "net.corda.irs"), networkSendManuallyPumped = networkSendManuallyPumped, - threadPerNode = runAsync, - cordappPackages = listOf("net.corda.finance.contract", "net.corda.irs")) + threadPerNode = runAsync) // TODO: Regulatory nodes don't actually exist properly, this is a last minute demo request. // So we just fire a message at a node that doesn't know how to handle it, and it'll ignore it. // But that's fine for visualisation purposes. diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt index 132636f05a..9f5c8802e4 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt @@ -14,7 +14,7 @@ import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import java.nio.file.Paths -fun main(args: Array) = BFTNotaryCordform().deployNodes() +fun main(args: Array) = BFTNotaryCordform().nodeRunner().deployAndRunNodes() private val clusterSize = 4 // Minimum size that tolerates a faulty replica. private val notaryNames = createNotaryNames(clusterSize) diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Clean.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Clean.kt index 91aa5ba967..2fbba77b26 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Clean.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Clean.kt @@ -1,9 +1,9 @@ package net.corda.notarydemo -import net.corda.testing.node.internal.demorun.clean +import net.corda.testing.node.internal.demorun.nodeRunner fun main(args: Array) { - listOf(SingleNotaryCordform(), RaftNotaryCordform(), BFTNotaryCordform()).forEach { + listOf(SingleNotaryCordform(), RaftNotaryCordform(), BFTNotaryCordform()).map { it.nodeRunner() }.forEach { it.clean() } } diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/CustomNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/CustomNotaryCordform.kt index e9a857b1cb..3aa6b38654 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/CustomNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/CustomNotaryCordform.kt @@ -9,7 +9,7 @@ import net.corda.testing.core.BOB_NAME import net.corda.testing.core.DUMMY_NOTARY_NAME import java.nio.file.Paths -fun main(args: Array) = CustomNotaryCordform().deployNodes() +fun main(args: Array) = CustomNotaryCordform().nodeRunner().deployAndRunNodes() class CustomNotaryCordform : CordformDefinition() { init { diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt index 1dd9cda94b..e79c54e5bf 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt @@ -4,12 +4,16 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.TimeWindow import net.corda.core.contracts.TransactionVerificationException import net.corda.core.flows.* +import net.corda.core.flows.NotarisationPayload +import net.corda.core.flows.NotarisationRequest +import net.corda.core.internal.ResolveTransactionsFlow +import net.corda.core.internal.validateRequest import net.corda.core.node.AppServiceHub import net.corda.core.node.services.CordaService -import net.corda.core.node.services.TimeWindowChecker import net.corda.core.node.services.TrustedAuthorityNotaryService -import net.corda.core.transactions.LedgerTransaction +import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionWithSignatures +import net.corda.core.utilities.unwrap import net.corda.node.services.transactions.PersistentUniquenessProvider import java.security.PublicKey import java.security.SignatureException @@ -17,7 +21,8 @@ import java.security.SignatureException /** * A custom notary service should provide a constructor that accepts two parameters of types [AppServiceHub] and [PublicKey]. * - * Note that at present only a single-node notary service can be customised. + * Note that the support for custom notaries is still experimental – at present only a single-node notary service can be customised. + * The notary-related APIs might change in the future. */ // START 1 @CordaService @@ -41,19 +46,15 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating @Suspendable override fun receiveAndVerifyTx(): TransactionParts { try { - val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false)) + val stx = receiveTransaction() val notary = stx.notary checkNotary(notary) - var timeWindow: TimeWindow? = null - val transactionWithSignatures = if (stx.isNotaryChangeTransaction()) { - stx.resolveNotaryChangeTransaction(serviceHub) - } else { - val wtx = stx.tx - customVerify(wtx.toLedgerTransaction(serviceHub)) - timeWindow = wtx.timeWindow - stx - } - checkSignatures(transactionWithSignatures) + val timeWindow: TimeWindow? = if (stx.isNotaryChangeTransaction()) + null + else + stx.tx.timeWindow + resolveAndContractVerify(stx) + verifySignatures(stx) return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!) } catch (e: Exception) { throw when (e) { @@ -64,8 +65,25 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating } } - private fun customVerify(transaction: LedgerTransaction) { - // Add custom verification logic + @Suspendable + private fun receiveTransaction(): SignedTransaction { + return otherSideSession.receive().unwrap { + val stx = it.signedTransaction + validateRequest(NotarisationRequest(stx.inputs, stx.id), it.requestSignature) + stx + } + } + + @Suspendable + private fun resolveAndContractVerify(stx: SignedTransaction) { + subFlow(ResolveTransactionsFlow(stx, otherSideSession)) + stx.verify(serviceHub, false) + customVerify(stx) + } + + private fun verifySignatures(stx: SignedTransaction) { + val transactionWithSignatures = stx.resolveTransactionWithSignatures(serviceHub) + checkSignatures(transactionWithSignatures) } private fun checkSignatures(tx: TransactionWithSignatures) { @@ -75,5 +93,9 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating throw NotaryException(NotaryError.TransactionInvalid(e)) } } + + private fun customVerify(stx: SignedTransaction) { + // Add custom verification logic + } } // END 2 diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt index c4cbcd0d8e..2a2aec2582 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt @@ -13,7 +13,7 @@ import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import java.nio.file.Paths -fun main(args: Array) = RaftNotaryCordform().deployNodes() +fun main(args: Array) = RaftNotaryCordform().nodeRunner().deployAndRunNodes() internal fun createNotaryNames(clusterSize: Int) = (0 until clusterSize).map { CordaX500Name("Notary Service $it", "Zurich", "CH") } diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt index af449ce5cb..d5acdb135c 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt @@ -11,7 +11,7 @@ import net.corda.testing.node.User import net.corda.testing.node.internal.demorun.* import java.nio.file.Paths -fun main(args: Array) = SingleNotaryCordform().deployNodes() +fun main(args: Array) = SingleNotaryCordform().nodeRunner().deployAndRunNodes() val notaryDemoUser = User("demou", "demop", setOf(all())) diff --git a/samples/simm-valuation-demo/build.gradle b/samples/simm-valuation-demo/build.gradle index b560d1aa4f..9edd4f2853 100644 --- a/samples/simm-valuation-demo/build.gradle +++ b/samples/simm-valuation-demo/build.gradle @@ -68,7 +68,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { name "O=Notary Service,L=Zurich,C=CH" notary = [validating : true] p2pPort 10002 - cordapps = ["$project.group:finance:$corda_release_version"] + cordapp project(':finance') extraConfig = [ jvmArgs : [ "-Xmx1g"] ] @@ -81,7 +81,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { address("localhost:10016") adminAddress("localhost:10017") } - cordapps = ["$project.group:finance:$corda_release_version"] + cordapp project(':finance') rpcUsers = ext.rpcUsers extraConfig = [ jvmArgs : [ "-Xmx1g"] @@ -95,7 +95,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { address("localhost:10026") adminAddress("localhost:10027") } - cordapps = ["$project.group:finance:$corda_release_version"] + cordapp project(':finance') rpcUsers = ext.rpcUsers extraConfig = [ jvmArgs : [ "-Xmx1g"] @@ -109,7 +109,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { address("localhost:10036") adminAddress("localhost:10037") } - cordapps = ["$project.group:finance:$corda_release_version"] + cordapp project(':finance') rpcUsers = ext.rpcUsers extraConfig = [ jvmArgs : [ "-Xmx1g"] diff --git a/settings.gradle b/settings.gradle index 4b0a92c815..1191e87414 100644 --- a/settings.gradle +++ b/settings.gradle @@ -46,3 +46,4 @@ include 'samples:network-visualiser' include 'samples:simm-valuation-demo' include 'samples:notary-demo' include 'samples:bank-of-corda-demo' +include 'samples:cordapp-configuration' diff --git a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt index 3a08b300c9..47232d4458 100644 --- a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt +++ b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt @@ -3,6 +3,7 @@ package net.corda.testing.driver import net.corda.core.concurrent.CordaFuture import net.corda.core.identity.CordaX500Name import net.corda.core.internal.CertRole +import net.corda.core.internal.concurrent.transpose import net.corda.core.internal.div import net.corda.core.internal.list import net.corda.core.internal.readLines @@ -14,15 +15,16 @@ import net.corda.testing.node.internal.addressMustBeBound import net.corda.testing.node.internal.addressMustNotBeBound import net.corda.testing.node.internal.internalDriver import net.corda.testing.core.DUMMY_BANK_A_NAME +import net.corda.testing.core.DUMMY_BANK_B_NAME import net.corda.testing.core.DUMMY_NOTARY_NAME -import net.corda.testing.driver.internal.NodeHandleInternal import net.corda.testing.http.HttpApi import net.corda.testing.node.NotarySpec -import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.* import org.json.simple.JSONObject import org.junit.Test import java.util.concurrent.Executors import java.util.concurrent.ScheduledExecutorService +import kotlin.streams.toList class DriverTests { private companion object { @@ -91,10 +93,11 @@ class DriverTests { @Test fun `monitoring mode enables jolokia exporting of JMX metrics via HTTP JSON`() { - driver(DriverParameters(jmxPolicy = JmxPolicy(true))) { + driver(DriverParameters(startNodesInProcess = false)) { // start another node so we gain access to node JMX metrics - startNode(providedName = DUMMY_REGULATOR_NAME).getOrThrow() val webAddress = NetworkHostAndPort("localhost", 7006) + startNode(providedName = DUMMY_REGULATOR_NAME, + customOverrides = mapOf("jmxMonitoringHttpPort" to webAddress.port)).getOrThrow() // request access to some JMX metrics via Jolokia HTTP/JSON val api = HttpApi.fromHostAndPort(webAddress, "/jolokia/") val versionAsJson = api.getJson("/jolokia/version/") @@ -116,4 +119,39 @@ class DriverTests { } assertThat(baseDirectory / "process-id").doesNotExist() } -} + + @Test + fun `driver rejects multiple nodes with the same name`() { + + driver(DriverParameters(startNodesInProcess = true)) { + + assertThatThrownBy { listOf(newNode(DUMMY_BANK_A_NAME)(), newNode(DUMMY_BANK_B_NAME)(), newNode(DUMMY_BANK_A_NAME)()).transpose().getOrThrow() }.isInstanceOf(IllegalArgumentException::class.java) + } + } + + @Test + fun `driver rejects multiple nodes with the same name parallel`() { + + driver(DriverParameters(startNodesInProcess = true)) { + + val nodes = listOf(newNode(DUMMY_BANK_A_NAME), newNode(DUMMY_BANK_B_NAME), newNode(DUMMY_BANK_A_NAME)) + + assertThatThrownBy { nodes.parallelStream().map { it.invoke() }.toList().transpose().getOrThrow() }.isInstanceOf(IllegalArgumentException::class.java) + } + } + + @Test + fun `driver allows reusing names of nodes that have been stopped`() { + + driver(DriverParameters(startNodesInProcess = true)) { + + val nodeA = newNode(DUMMY_BANK_A_NAME)().getOrThrow() + + nodeA.stop() + + assertThatCode { newNode(DUMMY_BANK_A_NAME)().getOrThrow() }.doesNotThrowAnyException() + } + } + + private fun DriverDSL.newNode(name: CordaX500Name) = { startNode(NodeParameters(providedName = name)) } +} \ No newline at end of file diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt index 883d61b88f..cf9d5f216d 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -8,12 +8,13 @@ import net.corda.core.flows.FlowLogic import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.messaging.CordaRPCOps +import net.corda.core.node.NetworkParameters import net.corda.core.node.NodeInfo import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.internal.Node import net.corda.node.services.api.StartedNodeServices import net.corda.node.services.config.VerifierType -import net.corda.nodeapi.internal.persistence.CordaPersistence +import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.node.NotarySpec import net.corda.testing.node.User @@ -58,7 +59,6 @@ interface OutOfProcess : NodeHandle { @DoNotImplement interface InProcess : NodeHandle { - val database: CordaPersistence val services: StartedNodeServices /** * Register a flow that is initiated by another flow @@ -101,14 +101,20 @@ data class NodeParameters( val startInSameProcess: Boolean? = null, val maximumHeapSize: String = "200m" ) { - fun setProvidedName(providedName: CordaX500Name?): NodeParameters = copy(providedName = providedName) - fun setRpcUsers(rpcUsers: List): NodeParameters = copy(rpcUsers = rpcUsers) - fun setVerifierType(verifierType: VerifierType): NodeParameters = copy(verifierType = verifierType) - fun setCustomerOverrides(customOverrides: Map): NodeParameters = copy(customOverrides = customOverrides) - fun setStartInSameProcess(startInSameProcess: Boolean?): NodeParameters = copy(startInSameProcess = startInSameProcess) - fun setMaximumHeapSize(maximumHeapSize: String): NodeParameters = copy(maximumHeapSize = maximumHeapSize) + fun withProvidedName(providedName: CordaX500Name?): NodeParameters = copy(providedName = providedName) + fun withRpcUsers(rpcUsers: List): NodeParameters = copy(rpcUsers = rpcUsers) + fun withVerifierType(verifierType: VerifierType): NodeParameters = copy(verifierType = verifierType) + fun withCustomerOverrides(customOverrides: Map): NodeParameters = copy(customOverrides = customOverrides) + fun withStartInSameProcess(startInSameProcess: Boolean?): NodeParameters = copy(startInSameProcess = startInSameProcess) + fun withMaximumHeapSize(maximumHeapSize: String): NodeParameters = copy(maximumHeapSize = maximumHeapSize) } +/** + * @property startJmxHttpServer Indicates whether the spawned nodes should start with a Jolokia JMX agent to enable remote + * JMX monitoring using HTTP/JSON + * @property jmxHttpServerPortAllocation The port allocation strategy to use for remote Jolokia/JMX monitoring over HTTP. + * Defaults to incremental. + */ data class JmxPolicy(val startJmxHttpServer: Boolean = false, val jmxHttpServerPortAllocation: PortAllocation? = if (startJmxHttpServer) PortAllocation.Incremental(7005) else null) @@ -128,30 +134,10 @@ data class JmxPolicy(val startJmxHttpServer: Boolean = false, * * @param defaultParameters The default parameters for the driver. Allows the driver to be configured in builder style * when called from Java code. - * @param isDebug Indicates whether the spawned nodes should start in jdwt debug mode and have debug level logging. - * @param driverDirectory The base directory node directories go into, defaults to "build//". The node - * directories themselves are "//", where legalName defaults to "-" - * and may be specified in [DriverDSL.startNode]. - * @param portAllocation The port allocation strategy to use for the messaging and the web server addresses. Defaults to incremental. - * @param debugPortAllocation The port allocation strategy to use for jvm debugging. Defaults to incremental. - * @param systemProperties A Map of extra system properties which will be given to each new node. Defaults to empty. - * @param useTestClock If true the test clock will be used in Node. - * @param startNodesInProcess Provides the default behaviour of whether new nodes should start inside this process or - * not. Note that this may be overridden in [DriverDSL.startNode]. - * @param waitForAllNodesToFinish If true, the nodes will not shut down automatically after executing the code in the driver DSL block. - * It will wait for them to be shut down externally instead. - * @param notarySpecs The notaries advertised for this network. These nodes will be started automatically and will be - * available from [DriverDSL.notaryHandles]. Defaults to a simple validating notary. - * @param jmxPolicy Used to specify whether to expose JMX metrics via Jolokia HHTP/JSON. Defines two attributes: - * startJmxHttpServer: indicates whether the spawned nodes should start with a Jolokia JMX agent to enable remote JMX monitoring using HTTP/JSON. - * jmxHttpServerPortAllocation: the port allocation strategy to use for remote Jolokia/JMX monitoring over HTTP. Defaults to incremental. - * @param dsl The dsl itself. + * @property dsl The dsl itself. * @return The value returned in the [dsl] closure. */ -fun driver( - defaultParameters: DriverParameters = DriverParameters(), - dsl: DriverDSL.() -> A -): A { +fun driver(defaultParameters: DriverParameters = DriverParameters(), dsl: DriverDSL.() -> A): A { return genericDriver( driverDsl = DriverDSLImpl( portAllocation = defaultParameters.portAllocation, @@ -166,15 +152,35 @@ fun driver( extraCordappPackagesToScan = defaultParameters.extraCordappPackagesToScan, jmxPolicy = defaultParameters.jmxPolicy, compatibilityZone = null, - maxTransactionSize = defaultParameters.maxTransactionSize + networkParameters = defaultParameters.networkParameters ), coerce = { it }, dsl = dsl, - initialiseSerialization = defaultParameters.initialiseSerialization + initialiseSerialization = true ) } -/** Helper builder for configuring a [driver] from Java. */ +/** + * Builder for configuring a [driver]. + * @property isDebug Indicates whether the spawned nodes should start in jdwt debug mode and have debug level logging. + * @property driverDirectory The base directory node directories go into, defaults to "build//". The node + * directories themselves are "//", where legalName defaults to "-" + * and may be specified in [DriverDSL.startNode]. + * @property portAllocation The port allocation strategy to use for the messaging and the web server addresses. Defaults + * to incremental. + * @property debugPortAllocation The port allocation strategy to use for jvm debugging. Defaults to incremental. + * @property systemProperties A Map of extra system properties which will be given to each new node. Defaults to empty. + * @property useTestClock If true the test clock will be used in Node. + * @property startNodesInProcess Provides the default behaviour of whether new nodes should start inside this process or + * not. Note that this may be overridden in [DriverDSL.startNode]. + * @property waitForAllNodesToFinish If true, the nodes will not shut down automatically after executing the code in the + * driver DSL block. It will wait for them to be shut down externally instead. + * @property notarySpecs The notaries advertised for this network. These nodes will be started automatically and will be + * available from [DriverDSL.notaryHandles]. Defaults to a simple validating notary. + * @property jmxPolicy Used to specify whether to expose JMX metrics via Jolokia HHTP/JSON. + * @property networkParameters The network parmeters to be used by all the nodes. [NetworkParameters.notaries] must be + * empty as notaries are defined by [notarySpecs]. + */ @Suppress("unused") data class DriverParameters( val isDebug: Boolean = false, @@ -183,24 +189,23 @@ data class DriverParameters( val debugPortAllocation: PortAllocation = PortAllocation.Incremental(5005), val systemProperties: Map = emptyMap(), val useTestClock: Boolean = false, - val initialiseSerialization: Boolean = true, val startNodesInProcess: Boolean = false, val waitForAllNodesToFinish: Boolean = false, val notarySpecs: List = listOf(NotarySpec(DUMMY_NOTARY_NAME)), val extraCordappPackagesToScan: List = emptyList(), val jmxPolicy: JmxPolicy = JmxPolicy(), - val maxTransactionSize: Int = Int.MAX_VALUE + val networkParameters: NetworkParameters = testNetworkParameters() ) { - fun setIsDebug(isDebug: Boolean) = copy(isDebug = isDebug) - fun setDriverDirectory(driverDirectory: Path) = copy(driverDirectory = driverDirectory) - fun setPortAllocation(portAllocation: PortAllocation) = copy(portAllocation = portAllocation) - fun setDebugPortAllocation(debugPortAllocation: PortAllocation) = copy(debugPortAllocation = debugPortAllocation) - fun setSystemProperties(systemProperties: Map) = copy(systemProperties = systemProperties) - fun setUseTestClock(useTestClock: Boolean) = copy(useTestClock = useTestClock) - fun setInitialiseSerialization(initialiseSerialization: Boolean) = copy(initialiseSerialization = initialiseSerialization) - fun setStartNodesInProcess(startNodesInProcess: Boolean) = copy(startNodesInProcess = startNodesInProcess) - fun setWaitForAllNodesToFinish(waitForAllNodesToFinish: Boolean) = copy(waitForAllNodesToFinish = waitForAllNodesToFinish) - fun setNotarySpecs(notarySpecs: List) = copy(notarySpecs = notarySpecs) - fun setExtraCordappPackagesToScan(extraCordappPackagesToScan: List) = copy(extraCordappPackagesToScan = extraCordappPackagesToScan) - fun setJmxPolicy(jmxPolicy: JmxPolicy) = copy(jmxPolicy = jmxPolicy) -} \ No newline at end of file + fun withIsDebug(isDebug: Boolean): DriverParameters = copy(isDebug = isDebug) + fun withDriverDirectory(driverDirectory: Path): DriverParameters = copy(driverDirectory = driverDirectory) + fun withPortAllocation(portAllocation: PortAllocation): DriverParameters = copy(portAllocation = portAllocation) + fun withDebugPortAllocation(debugPortAllocation: PortAllocation): DriverParameters = copy(debugPortAllocation = debugPortAllocation) + fun withSystemProperties(systemProperties: Map): DriverParameters = copy(systemProperties = systemProperties) + fun withUseTestClock(useTestClock: Boolean): DriverParameters = copy(useTestClock = useTestClock) + fun withStartNodesInProcess(startNodesInProcess: Boolean): DriverParameters = copy(startNodesInProcess = startNodesInProcess) + fun withWaitForAllNodesToFinish(waitForAllNodesToFinish: Boolean): DriverParameters = copy(waitForAllNodesToFinish = waitForAllNodesToFinish) + fun withNotarySpecs(notarySpecs: List): DriverParameters = copy(notarySpecs = notarySpecs) + fun withExtraCordappPackagesToScan(extraCordappPackagesToScan: List): DriverParameters = copy(extraCordappPackagesToScan = extraCordappPackagesToScan) + fun withJmxPolicy(jmxPolicy: JmxPolicy): DriverParameters = copy(jmxPolicy = jmxPolicy) + fun withNetworkParameters(networkParameters: NetworkParameters): DriverParameters = copy(networkParameters = networkParameters) +} diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/internal/DriverInternal.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/internal/DriverInternal.kt index 3f658d6086..972677dd4d 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/internal/DriverInternal.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/internal/DriverInternal.kt @@ -15,6 +15,7 @@ import net.corda.testing.driver.OutOfProcess import net.corda.testing.node.User import rx.Observable import java.nio.file.Path +import java.sql.Connection interface NodeHandleInternal : NodeHandle { val configuration: NodeConfiguration @@ -57,7 +58,7 @@ data class InProcessImpl( private val onStopCallback: () -> Unit, private val node: StartedNode ) : InProcess, NodeHandleInternal { - override val database: CordaPersistence get() = node.database + val database: CordaPersistence = node.database override val services: StartedNodeServices get() = node.services override val rpcUsers: List = configuration.rpcUsers.map { User(it.username, it.password, it.permissions) } override fun stop() { diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt index 744ceb1dbd..70613b3253 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt @@ -1,5 +1,6 @@ package net.corda.testing.node +import net.corda.core.CordaInternal import net.corda.core.DoNotImplement import net.corda.core.crypto.CompositeKey import net.corda.core.identity.CordaX500Name @@ -48,7 +49,7 @@ import kotlin.concurrent.thread * a service is addressed. */ @ThreadSafe -class InMemoryMessagingNetwork internal constructor( +class InMemoryMessagingNetwork private constructor( private val sendManuallyPumped: Boolean, private val servicePeerAllocationStrategy: ServicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.Random(), private val messagesInFlight: ReusableLatch = ReusableLatch() @@ -56,6 +57,13 @@ class InMemoryMessagingNetwork internal constructor( companion object { private const val MESSAGES_LOG_NAME = "messages" private val log = LoggerFactory.getLogger(MESSAGES_LOG_NAME) + + internal fun create( + sendManuallyPumped: Boolean, + servicePeerAllocationStrategy: ServicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.Random(), + messagesInFlight: ReusableLatch = ReusableLatch()): InMemoryMessagingNetwork { + return InMemoryMessagingNetwork(sendManuallyPumped, servicePeerAllocationStrategy, messagesInFlight) + } } private var counter = 0 // -1 means stopped. @@ -115,7 +123,8 @@ class InMemoryMessagingNetwork internal constructor( val peerHandle = PeerHandle(id, description) peersMapping[peerHandle.description] = peerHandle // Assume that the same name - the same entity in MockNetwork. notaryService?.let { if (it.owningKey !is CompositeKey) peersMapping[it.name] = peerHandle } - val serviceHandles = notaryService?.let { listOf(ServiceHandle(it.party)) } ?: emptyList() //TODO only notary can be distributed? + val serviceHandles = notaryService?.let { listOf(ServiceHandle(it.party)) } + ?: emptyList() //TODO only notary can be distributed? synchronized(this) { val node = InMemoryMessaging(manuallyPumped, peerHandle, executor, database) handleEndpointMap[peerHandle] = node @@ -183,7 +192,8 @@ class InMemoryMessagingNetwork internal constructor( } /** - * Mock service loadbalancing + * How traffic is allocated in the case where multiple nodes share a single identity, which happens for notaries + * in a cluster. You don't normally ever need to change this: it is mostly useful for testing notary implementations. */ @DoNotImplement sealed class ServicePeerAllocationStrategy { @@ -304,7 +314,8 @@ class InMemoryMessagingNetwork internal constructor( override fun getAddressOfParty(partyInfo: PartyInfo): MessageRecipients { return when (partyInfo) { - is PartyInfo.SingleNode -> peersMapping[partyInfo.party.name] ?: throw IllegalArgumentException("No StartedMockNode for party ${partyInfo.party.name}") + is PartyInfo.SingleNode -> peersMapping[partyInfo.party.name] + ?: throw IllegalArgumentException("No StartedMockNode for party ${partyInfo.party.name}") is PartyInfo.DistributedNode -> ServiceHandle(partyInfo.party) } } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt index 66a930ce3e..d550695bc5 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt @@ -6,13 +6,14 @@ import net.corda.core.crypto.random63BitValue import net.corda.core.flows.FlowLogic import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party +import net.corda.core.node.NetworkParameters import net.corda.core.node.NodeInfo import net.corda.node.VersionInfo import net.corda.node.internal.StartedNode import net.corda.node.services.api.StartedNodeServices import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.MessagingService -import net.corda.nodeapi.internal.persistence.CordaPersistence +import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.setMessagingServiceSpy @@ -37,35 +38,51 @@ data class MockNodeParameters( val entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue()), val configOverrides: (NodeConfiguration) -> Any? = {}, val version: VersionInfo = MockServices.MOCK_VERSION_INFO) { - fun setForcedID(forcedID: Int?): MockNodeParameters = copy(forcedID = forcedID) - fun setLegalName(legalName: CordaX500Name?): MockNodeParameters = copy(legalName = legalName) - fun setEntropyRoot(entropyRoot: BigInteger): MockNodeParameters = copy(entropyRoot = entropyRoot) - fun setConfigOverrides(configOverrides: (NodeConfiguration) -> Any?): MockNodeParameters = copy(configOverrides = configOverrides) + fun withForcedID(forcedID: Int?): MockNodeParameters = copy(forcedID = forcedID) + fun withLegalName(legalName: CordaX500Name?): MockNodeParameters = copy(legalName = legalName) + fun withEntropyRoot(entropyRoot: BigInteger): MockNodeParameters = copy(entropyRoot = entropyRoot) + fun withConfigOverrides(configOverrides: (NodeConfiguration) -> Any?): MockNodeParameters = copy(configOverrides = configOverrides) } -/** Helper builder for configuring a [InternalMockNetwork] from Java. */ +/** + * Immutable builder for configuring a [MockNetwork]. Kotlin users can also use named parameters to the constructor + * of [MockNetwork], which is more convenient. + * + * @property networkSendManuallyPumped If true then messages will not be routed from sender to receiver until you use + * the [MockNetwork.runNetwork] method. This is useful for writing single-threaded unit test code that can examine the + * state of the mock network before and after a message is sent, without races and without the receiving node immediately + * sending a response. The default is false, so you must call runNetwork. + * @property threadPerNode If true then each node will be run in its own thread. This can result in race conditions in + * your code if not carefully written, but is more realistic and may help if you have flows in your app that do long + * blocking operations. The default is false. + * @property servicePeerAllocationStrategy How messages are load balanced in the case where a single compound identity + * is used by multiple nodes. You rarely if ever need to change that, it's primarily of interest to people testing + * notary code. + * @property notarySpecs The notaries to use in the mock network. By default you get one mock notary and that is usually sufficient. + * @property networkParameters The network parameters to be used by all the nodes. [NetworkParameters.notaries] must be + * empty as notaries are defined by [notarySpecs]. + */ @Suppress("unused") data class MockNetworkParameters( val networkSendManuallyPumped: Boolean = false, val threadPerNode: Boolean = false, val servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.Random(), - val initialiseSerialization: Boolean = true, val notarySpecs: List = listOf(MockNetworkNotarySpec(DUMMY_NOTARY_NAME)), - val maxTransactionSize: Int = Int.MAX_VALUE) { - fun setNetworkSendManuallyPumped(networkSendManuallyPumped: Boolean): MockNetworkParameters = copy(networkSendManuallyPumped = networkSendManuallyPumped) - fun setThreadPerNode(threadPerNode: Boolean): MockNetworkParameters = copy(threadPerNode = threadPerNode) - fun setServicePeerAllocationStrategy(servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy): MockNetworkParameters = copy(servicePeerAllocationStrategy = servicePeerAllocationStrategy) - fun setInitialiseSerialization(initialiseSerialization: Boolean): MockNetworkParameters = copy(initialiseSerialization = initialiseSerialization) - fun setNotarySpecs(notarySpecs: List): MockNetworkParameters = copy(notarySpecs = notarySpecs) - fun setMaxTransactionSize(maxTransactionSize: Int): MockNetworkParameters = copy(maxTransactionSize = maxTransactionSize) + val networkParameters: NetworkParameters = testNetworkParameters() +) { + fun withNetworkParameters(networkParameters: NetworkParameters): MockNetworkParameters = copy(networkParameters = networkParameters) + fun withNetworkSendManuallyPumped(networkSendManuallyPumped: Boolean): MockNetworkParameters = copy(networkSendManuallyPumped = networkSendManuallyPumped) + fun withThreadPerNode(threadPerNode: Boolean): MockNetworkParameters = copy(threadPerNode = threadPerNode) + fun withServicePeerAllocationStrategy(servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy): MockNetworkParameters = copy(servicePeerAllocationStrategy = servicePeerAllocationStrategy) + fun withNotarySpecs(notarySpecs: List): MockNetworkParameters = copy(notarySpecs = notarySpecs) } -/** Represents a node configuration for injection via [MockNetworkParameters] **/ +/** Represents a node configuration for injection via [MockNetworkParameters]. */ data class MockNetworkNotarySpec(val name: CordaX500Name, val validating: Boolean = true) { constructor(name: CordaX500Name) : this(name, validating = true) } -/** A class that represents an unstarted mock node for testing. **/ +/** A class that represents an unstarted mock node for testing. */ class UnstartedMockNode private constructor(private val node: InternalMockNetwork.MockNode) { companion object { internal fun create(node: InternalMockNetwork.MockNode): UnstartedMockNode { @@ -78,7 +95,7 @@ class UnstartedMockNode private constructor(private val node: InternalMockNetwor fun start() = StartedMockNode.create(node.start()) } -/** A class that represents a started mock node for testing. **/ +/** A class that represents a started mock node for testing. */ class StartedMockNode private constructor(private val node: StartedNode) { companion object { internal fun create(node: StartedNode): StartedMockNode { @@ -87,7 +104,6 @@ class StartedMockNode private constructor(private val node: StartedNode> findStateMachines(flowClass: Class): List>> = node.smm.findStateMachines(flowClass) + + fun transaction(statement: () -> T): T { + return node.database.transaction { + statement() + } + } } /** @@ -117,9 +139,14 @@ class StartedMockNode private constructor(private val node: StartedNode, val defaultParameters: MockNetworkParameters = MockNetworkParameters(), val networkSendManuallyPumped: Boolean = defaultParameters.networkSendManuallyPumped, val threadPerNode: Boolean = defaultParameters.threadPerNode, val servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy = defaultParameters.servicePeerAllocationStrategy, - val initialiseSerialization: Boolean = defaultParameters.initialiseSerialization, val notarySpecs: List = defaultParameters.notarySpecs, - val maxTransactionSize: Int = defaultParameters.maxTransactionSize) { + val networkParameters: NetworkParameters = defaultParameters.networkParameters) { @JvmOverloads constructor(cordappPackages: List, parameters: MockNetworkParameters = MockNetworkParameters()) : this(cordappPackages, defaultParameters = parameters) - private val internalMockNetwork: InternalMockNetwork = InternalMockNetwork(cordappPackages, defaultParameters, networkSendManuallyPumped, threadPerNode, servicePeerAllocationStrategy, initialiseSerialization, notarySpecs, maxTransactionSize) - val defaultNotaryNode get() : StartedMockNode = StartedMockNode.create(internalMockNetwork.defaultNotaryNode) - val defaultNotaryIdentity get() : Party = internalMockNetwork.defaultNotaryIdentity - val notaryNodes get() : List = internalMockNetwork.notaryNodes.map { StartedMockNode.create(it) } - val nextNodeId get() : Int = internalMockNetwork.nextNodeId + private val internalMockNetwork: InternalMockNetwork = InternalMockNetwork(cordappPackages, defaultParameters, networkSendManuallyPumped, threadPerNode, servicePeerAllocationStrategy, notarySpecs, networkParameters) + + /** Which node will be used as the primary notary during transaction builds. */ + val defaultNotaryNode get(): StartedMockNode = StartedMockNode.create(internalMockNetwork.defaultNotaryNode) + /** The [Party] of the [defaultNotaryNode] */ + val defaultNotaryIdentity get(): Party = internalMockNetwork.defaultNotaryIdentity + /** A list of all notary nodes in the network that have been started. */ + val notaryNodes get(): List = internalMockNetwork.notaryNodes.map { StartedMockNode.create(it) } + /** In a mock network, nodes have an incrementing integer ID. Real networks do not have this. Returns the next ID that will be used. */ + val nextNodeId get(): Int = internalMockNetwork.nextNodeId /** Create a started node with the given identity. **/ fun createPartyNode(legalName: CordaX500Name? = null): StartedMockNode = StartedMockNode.create(internalMockNetwork.createPartyNode(legalName)) @@ -152,7 +184,9 @@ open class MockNetwork( /** Create a started node with the given parameters. **/ fun createNode(parameters: MockNodeParameters = MockNodeParameters()): StartedMockNode = StartedMockNode.create(internalMockNetwork.createNode(parameters)) - /** Create a started node with the given parameters. + /** + * Create a started node with the given parameters. + * * @param legalName the node's legal name. * @param forcedID a unique identifier for the node. * @param entropyRoot the initial entropy value to use when generating keys. Defaults to an (insecure) random value, @@ -173,7 +207,9 @@ open class MockNetwork( /** Create an unstarted node with the given parameters. **/ fun createUnstartedNode(parameters: MockNodeParameters = MockNodeParameters()): UnstartedMockNode = UnstartedMockNode.create(internalMockNetwork.createUnstartedNode(parameters)) - /** Create an unstarted node with the given parameters. + /** + * Create an unstarted node with the given parameters. + * * @param legalName the node's legal name. * @param forcedID a unique identifier for the node. * @param entropyRoot the initial entropy value to use when generating keys. Defaults to an (insecure) random value, @@ -211,4 +247,4 @@ open class MockNetwork( /** Get the base directory for the given node id. **/ fun baseDirectory(nodeId: Int): Path = internalMockNetwork.baseDirectory(nodeId) -} \ No newline at end of file +} diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt index a86f4e380b..f553047f7c 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -1,12 +1,12 @@ package net.corda.testing.node import com.google.common.collect.MutableClassToInstanceMap +import net.corda.core.contracts.ContractClassName import net.corda.core.cordapp.CordappProvider import net.corda.core.crypto.* import net.corda.core.flows.FlowLogic import net.corda.core.identity.CordaX500Name import net.corda.core.identity.PartyAndCertificate -import net.corda.core.internal.GlobalProperties import net.corda.core.messaging.DataFeed import net.corda.core.messaging.FlowHandle import net.corda.core.messaging.FlowProgressHandle @@ -15,6 +15,7 @@ import net.corda.core.node.services.* import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.VersionInfo import net.corda.node.internal.configureDatabase import net.corda.node.internal.cordapp.CordappLoader @@ -32,10 +33,10 @@ import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.HibernateConfiguration import net.corda.testing.common.internal.testNetworkParameters -import net.corda.testing.core.DEV_ROOT_CA import net.corda.testing.core.TestIdentity +import net.corda.testing.internal.DEV_ROOT_CA +import net.corda.testing.internal.MockCordappProvider import net.corda.testing.services.MockAttachmentStorage -import net.corda.testing.services.MockCordappProvider import org.bouncycastle.operator.ContentSigner import rx.Observable import rx.subjects.PublishSubject @@ -61,6 +62,7 @@ open class MockServices private constructor( cordappLoader: CordappLoader, override val validatedTransactions: WritableTransactionStorage, override val identityService: IdentityService, + final override val networkParameters: NetworkParameters, private val initialIdentity: TestIdentity, private val moreKeys: Array ) : ServiceHub, StateLoader by validatedTransactions { @@ -94,16 +96,18 @@ open class MockServices private constructor( * @return a pair where the first element is the instance of [CordaPersistence] and the second is [MockServices]. */ @JvmStatic + @JvmOverloads fun makeTestDatabaseAndMockServices(cordappPackages: List, identityService: IdentityService, initialIdentity: TestIdentity, + networkParameters: NetworkParameters = testNetworkParameters(), vararg moreKeys: KeyPair): Pair { val cordappLoader = CordappLoader.createWithTestPackages(cordappPackages) val dataSourceProps = makeTestDataSourceProperties() val schemaService = NodeSchemaService(cordappLoader.cordappSchemas) val database = configureDatabase(dataSourceProps, DatabaseConfig(), identityService, schemaService) val mockService = database.transaction { - object : MockServices(cordappLoader, identityService, initialIdentity, moreKeys) { + object : MockServices(cordappLoader, identityService, networkParameters, initialIdentity, moreKeys) { override val vaultService: VaultServiceInternal = makeVaultService(database.hibernateConfig, schemaService) override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable) { @@ -127,30 +131,43 @@ open class MockServices private constructor( } } - private constructor(cordappLoader: CordappLoader, identityService: IdentityService, + private constructor(cordappLoader: CordappLoader, identityService: IdentityService, networkParameters: NetworkParameters, initialIdentity: TestIdentity, moreKeys: Array) - : this(cordappLoader, MockTransactionStorage(), identityService, initialIdentity, moreKeys) + : this(cordappLoader, MockTransactionStorage(), identityService, networkParameters, initialIdentity, moreKeys) /** * Create a mock [ServiceHub] that looks for app code in the given package names, uses the provided identity service * (you can get one from [makeTestIdentityService]) and represents the given identity. */ @JvmOverloads - constructor(cordappPackages: List, initialIdentity: TestIdentity, identityService: IdentityService = makeTestIdentityService(), vararg moreKeys: KeyPair) : this(CordappLoader.createWithTestPackages(cordappPackages), identityService, initialIdentity, moreKeys) + constructor(cordappPackages: List, + initialIdentity: TestIdentity, + identityService: IdentityService = makeTestIdentityService(), + vararg moreKeys: KeyPair) : + this(CordappLoader.createWithTestPackages(cordappPackages), identityService, testNetworkParameters(), initialIdentity, moreKeys) + + constructor(cordappPackages: List, + initialIdentity: TestIdentity, + identityService: IdentityService, + networkParameters: NetworkParameters, + vararg moreKeys: KeyPair) : + this(CordappLoader.createWithTestPackages(cordappPackages), identityService, networkParameters, initialIdentity, moreKeys) /** * Create a mock [ServiceHub] that looks for app code in the given package names, uses the provided identity service * (you can get one from [makeTestIdentityService]) and represents the given identity. */ @JvmOverloads - constructor(cordappPackages: List, initialIdentityName: CordaX500Name, identityService: IdentityService = makeTestIdentityService(), key: KeyPair, vararg moreKeys: KeyPair) : this(cordappPackages, TestIdentity(initialIdentityName, key), identityService, *moreKeys) + constructor(cordappPackages: List, initialIdentityName: CordaX500Name, identityService: IdentityService = makeTestIdentityService(), key: KeyPair, vararg moreKeys: KeyPair) : + this(cordappPackages, TestIdentity(initialIdentityName, key), identityService, *moreKeys) /** * Create a mock [ServiceHub] that can't load CorDapp code, which uses the provided identity service * (you can get one from [makeTestIdentityService]) and which represents the given identity. */ @JvmOverloads - constructor(cordappPackages: List, initialIdentityName: CordaX500Name, identityService: IdentityService = makeTestIdentityService()) : this(cordappPackages, TestIdentity(initialIdentityName), identityService) + constructor(cordappPackages: List, initialIdentityName: CordaX500Name, identityService: IdentityService = makeTestIdentityService()) : + this(cordappPackages, TestIdentity(initialIdentityName), identityService) /** * Create a mock [ServiceHub] that can't load CorDapp code, and which uses a default service identity. @@ -206,10 +223,10 @@ open class MockServices private constructor( override val clock: Clock get() = Clock.systemUTC() override val myInfo: NodeInfo get() { - return NodeInfo(emptyList(), listOf(initialIdentity.identity), 1, serial = 1L) + return NodeInfo(listOf(NetworkHostAndPort("mock.node.services", 10000)), listOf(initialIdentity.identity), 1, serial = 1L) } override val transactionVerifierService: TransactionVerifierService get() = InMemoryTransactionVerifierService(2) - val 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 { @@ -221,12 +238,17 @@ open class MockServices private constructor( val cordappServices: MutableClassToInstanceMap = MutableClassToInstanceMap.create() override fun cordaService(type: Class): T { require(type.isAnnotationPresent(CordaService::class.java)) { "${type.name} is not a Corda service" } - return cordappServices.getInstance(type) ?: throw IllegalArgumentException("Corda service ${type.name} does not exist") + return cordappServices.getInstance(type) + ?: throw IllegalArgumentException("Corda service ${type.name} does not exist") } override fun jdbcSession(): Connection = throw UnsupportedOperationException() override fun registerUnloadHandler(runOnStop: () -> Unit) = throw UnsupportedOperationException() + + fun addMockCordapp(contractClassName: ContractClassName) { + mockCordappProvider.addMockCordapp(contractClassName, attachments) + } } class MockKeyManagementService(val identityService: IdentityService, @@ -252,7 +274,8 @@ class MockKeyManagementService(val identityService: IdentityService, private fun getSigner(publicKey: PublicKey): ContentSigner = getSigner(getSigningKeyPair(publicKey)) private fun getSigningKeyPair(publicKey: PublicKey): KeyPair { - val pk = publicKey.keys.firstOrNull { keyStore.containsKey(it) } ?: throw IllegalArgumentException("Public key not found: ${publicKey.toStringShort()}") + val pk = publicKey.keys.firstOrNull { keyStore.containsKey(it) } + ?: throw IllegalArgumentException("Public key not found: ${publicKey.toStringShort()}") return KeyPair(pk, keyStore[pk]!!) } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeTestUtils.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeTestUtils.kt index 008a0c2e81..dcf47f96b3 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeTestUtils.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeTestUtils.kt @@ -9,13 +9,11 @@ import net.corda.core.context.InvocationContext import net.corda.core.flows.FlowLogic import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party -import net.corda.core.internal.GlobalProperties import net.corda.core.node.ServiceHub import net.corda.core.serialization.internal.effectiveSerializationEnv import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.getOrThrow import net.corda.node.services.api.StartedNodeServices -import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.TestIdentity import net.corda.testing.core.chooseIdentity @@ -36,7 +34,6 @@ fun ServiceHub.ledger( false } return LedgerDSL(TestLedgerDSLInterpreter(this), notary).apply { - GlobalProperties.networkParameters = testNetworkParameters(emptyList()) if (serializationExists) { script() } else { diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/TestClock.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/TestClock.kt index 75d6563ae4..c99f1d128d 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/TestClock.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/TestClock.kt @@ -1,7 +1,7 @@ package net.corda.testing.node import net.corda.core.internal.until -import net.corda.node.internal.MutableClock +import net.corda.node.MutableClock import java.time.Clock import java.time.Duration import java.time.Instant diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index eb3ae63268..e3879eef58 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -15,14 +15,15 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.internal.* import net.corda.core.internal.concurrent.* import net.corda.core.messaging.CordaRPCOps +import net.corda.core.node.NetworkParameters import net.corda.core.node.NotaryInfo import net.corda.core.node.services.NetworkMapCache -import net.corda.core.serialization.deserialize import net.corda.core.toFuture import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.millis +import net.corda.node.NodeRegistrationOption import net.corda.node.internal.Node import net.corda.node.internal.NodeStartup import net.corda.node.internal.StartedNode @@ -39,15 +40,14 @@ import net.corda.nodeapi.internal.crypto.X509KeyStore import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.nodeapi.internal.network.NodeInfoFilesCopier -import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.core.DUMMY_BANK_A_NAME -import net.corda.testing.core.setGlobalSerialization import net.corda.testing.driver.* import net.corda.testing.driver.internal.InProcessImpl import net.corda.testing.driver.internal.NodeHandleInternal import net.corda.testing.driver.internal.OutOfProcessImpl +import net.corda.testing.internal.setGlobalSerialization import net.corda.testing.node.ClusterSpec import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO import net.corda.testing.node.NotarySpec @@ -92,7 +92,7 @@ class DriverDSLImpl( val jmxPolicy: JmxPolicy, val notarySpecs: List, val compatibilityZone: CompatibilityZoneParams?, - val maxTransactionSize: Int + val networkParameters: NetworkParameters ) : InternalDriverDSL { private var _executorService: ScheduledExecutorService? = null val executorService get() = _executorService!! @@ -101,6 +101,7 @@ class DriverDSLImpl( private val cordappPackages = extraCordappPackagesToScan + getCallerPackage() // Map from a nodes legal name to an observable emitting the number of nodes in its network map. private val countObservables = mutableMapOf>() + private val nodeNames = mutableSetOf() /** * Future which completes when the network map is available, whether a local one or one from the CZ. This future acts * as a gate to prevent nodes from starting too early. The value of the future is a [LocalNetworkMap] object, which @@ -186,6 +187,12 @@ class DriverDSLImpl( val p2pAddress = portAllocation.nextHostAndPort() // TODO: Derive name from the full picked name, don't just wrap the common name val name = providedName ?: CordaX500Name("${oneOf(names).organisation}-${p2pAddress.port}", "London", "GB") + synchronized(nodeNames) { + val wasANewNode = nodeNames.add(name) + if (!wasANewNode) { + throw IllegalArgumentException("Node with name $name is already started or starting.") + } + } val registrationFuture = if (compatibilityZone?.rootCert != null) { // We don't need the network map to be available to be able to register the node startNodeRegistration(name, compatibilityZone.rootCert, compatibilityZone.url) @@ -253,7 +260,7 @@ class DriverDSLImpl( return if (startNodesInProcess) { executorService.fork { - NetworkRegistrationHelper(config.corda, HTTPNetworkRegistrationService(compatibilityZoneURL), rootTruststorePath, rootTruststorePassword).buildKeystore() + NetworkRegistrationHelper(config.corda, HTTPNetworkRegistrationService(compatibilityZoneURL), NodeRegistrationOption(rootTruststorePath, rootTruststorePassword)).buildKeystore() config } } else { @@ -387,6 +394,7 @@ class DriverDSLImpl( if (startNodesInProcess) { Schedulers.reset() } + require(networkParameters.notaries.isEmpty()) { "Define notaries using notarySpecs" } _executorService = Executors.newScheduledThreadPool(2, ThreadFactoryBuilder().setNameFormat("driver-pool-thread-%d").build()) _shutdownManager = ShutdownManager(executorService) val notaryInfosFuture = if (compatibilityZone == null) { @@ -476,7 +484,7 @@ class DriverDSLImpl( val nodeInfoFile = config.corda.baseDirectory.list { paths -> paths.filter { it.fileName.toString().startsWith(NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX) }.findFirst().get() } - val nodeInfo = nodeInfoFile.readAll().deserialize().verified() + val nodeInfo = nodeInfoFile.readObject().verified() NotaryInfo(nodeInfo.legalIdentities[0], spec.validating) } } @@ -641,6 +649,9 @@ class DriverDSLImpl( val onNodeExit: () -> Unit = { localNetworkMap?.nodeInfosCopier?.removeConfig(baseDirectory) countObservables.remove(config.corda.myLegalName) + synchronized(nodeNames) { + nodeNames.remove(config.corda.myLegalName) + } } val useHTTPS = config.typesafe.run { hasPath("useHTTPS") && getBoolean("useHTTPS") } @@ -704,7 +715,7 @@ class DriverDSLImpl( * The local version of the network map, which is a bunch of classes that copy the relevant files to the node directories. */ private inner class LocalNetworkMap(notaryInfos: List) { - val networkParametersCopier = NetworkParametersCopier(testNetworkParameters(notaryInfos, maxTransactionSize = maxTransactionSize)) + val networkParametersCopier = NetworkParametersCopier(networkParameters.copy(notaries = notaryInfos)) // TODO: this object will copy NodeInfo files from started nodes to other nodes additional-node-infos/ // This uses the FileSystem and adds a delay (~5 seconds) given by the time we wait before polling the file system. // Investigate whether we can avoid that. @@ -951,7 +962,7 @@ fun genericDriver( driverDslWrapper: (DriverDSLImpl) -> D, coerce: (D) -> DI, dsl: DI.() -> A ): A { - val serializationEnv = setGlobalSerialization(defaultParameters.initialiseSerialization) + val serializationEnv = setGlobalSerialization(true) val driverDsl = driverDslWrapper( DriverDSLImpl( portAllocation = defaultParameters.portAllocation, @@ -966,7 +977,7 @@ fun genericDriver( jmxPolicy = defaultParameters.jmxPolicy, notarySpecs = defaultParameters.notarySpecs, compatibilityZone = null, - maxTransactionSize = defaultParameters.maxTransactionSize + networkParameters = defaultParameters.networkParameters ) ) val shutdownHook = addShutdownHook(driverDsl::shutdown) @@ -1002,13 +1013,13 @@ fun internalDriver( debugPortAllocation: PortAllocation = DriverParameters().debugPortAllocation, systemProperties: Map = DriverParameters().systemProperties, useTestClock: Boolean = DriverParameters().useTestClock, - initialiseSerialization: Boolean = DriverParameters().initialiseSerialization, + initialiseSerialization: Boolean = true, startNodesInProcess: Boolean = DriverParameters().startNodesInProcess, waitForAllNodesToFinish: Boolean = DriverParameters().waitForAllNodesToFinish, notarySpecs: List = DriverParameters().notarySpecs, extraCordappPackagesToScan: List = DriverParameters().extraCordappPackagesToScan, jmxPolicy: JmxPolicy = DriverParameters().jmxPolicy, - maxTransactionSize: Int = DriverParameters().maxTransactionSize, + networkParameters: NetworkParameters = DriverParameters().networkParameters, compatibilityZone: CompatibilityZoneParams? = null, dsl: DriverDSLImpl.() -> A ): A { @@ -1026,7 +1037,7 @@ fun internalDriver( extraCordappPackagesToScan = extraCordappPackagesToScan, jmxPolicy = jmxPolicy, compatibilityZone = compatibilityZone, - maxTransactionSize = maxTransactionSize + networkParameters = networkParameters ), coerce = { it }, dsl = dsl, diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt index b3366e8dbc..4c42d319ce 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt @@ -18,6 +18,7 @@ import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.MessageRecipients import net.corda.core.messaging.RPCOps import net.corda.core.messaging.SingleMessageRecipient +import net.corda.core.node.NetworkParameters import net.corda.core.node.NodeInfo import net.corda.core.node.NotaryInfo import net.corda.core.node.services.IdentityService @@ -32,11 +33,7 @@ import net.corda.node.internal.StartedNode import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.services.api.NodePropertiesStore import net.corda.node.services.api.SchemaService -import net.corda.node.services.config.BFTSMaRtConfiguration -import net.corda.node.services.config.CertChainPolicyConfig -import net.corda.node.services.config.NodeConfiguration -import net.corda.node.services.config.NotaryConfig -import net.corda.node.services.config.VerifierType +import net.corda.node.services.config.* import net.corda.node.services.keys.E2ETestKeyManagementService import net.corda.node.services.messaging.MessagingService import net.corda.node.services.transactions.BFTNonValidatingNotaryService @@ -50,17 +47,12 @@ import net.corda.nodeapi.internal.network.NetworkParametersCopier 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.setGlobalSerialization import net.corda.testing.internal.rigorousMock +import net.corda.testing.internal.setGlobalSerialization import net.corda.testing.internal.testThreadFactory -import net.corda.testing.node.InMemoryMessagingNetwork -import net.corda.testing.node.MessagingServiceSpy -import net.corda.testing.node.MockNetworkNotarySpec -import net.corda.testing.node.MockNetworkParameters -import net.corda.testing.node.MockNodeParameters +import net.corda.testing.node.* import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties -import net.corda.testing.node.TestClock import org.apache.activemq.artemis.utils.ReusableLatch import org.apache.sshd.common.util.security.SecurityUtils import rx.internal.schedulers.CachedThreadScheduler @@ -84,49 +76,33 @@ data class MockNodeArgs( val version: VersionInfo = MOCK_VERSION_INFO ) -/** - * A mock node brings up a suite of in-memory services in a fast manner suitable for unit testing. - * Components that do IO are either swapped out for mocks, or pointed to a [Jimfs] in memory filesystem or an in - * memory H2 database instance. - * - * Mock network nodes require manual pumping by default: they will not run asynchronous. This means that - * for message exchanges to take place (and associated handlers to run), you must call the [runNetwork] - * method. - * - * You can get a printout of every message sent by using code like: - * - * LogHelper.setLevel("+messages") - * - * By default a single notary node is automatically started, which forms part of the network parameters for all the nodes. - * This node is available by calling [defaultNotaryNode]. - */ open class InternalMockNetwork(private val cordappPackages: List, defaultParameters: MockNetworkParameters = MockNetworkParameters(), val networkSendManuallyPumped: Boolean = defaultParameters.networkSendManuallyPumped, val threadPerNode: Boolean = defaultParameters.threadPerNode, servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy = defaultParameters.servicePeerAllocationStrategy, - initialiseSerialization: Boolean = defaultParameters.initialiseSerialization, val notarySpecs: List = defaultParameters.notarySpecs, - maxTransactionSize: Int = Int.MAX_VALUE, + networkParameters: NetworkParameters = testNetworkParameters(), val defaultFactory: (MockNodeArgs) -> MockNode = InternalMockNetwork::MockNode) { init { // Apache SSHD for whatever reason registers a SFTP FileSystemProvider - which gets loaded by JimFS. // This SFTP support loads BouncyCastle, which we want to avoid. // Please see https://issues.apache.org/jira/browse/SSHD-736 - it's easier then to create our own fork of SSHD SecurityUtils.setAPrioriDisabledProvider("BC", true) // XXX: Why isn't this static? + require(networkParameters.notaries.isEmpty()) { "Define notaries using notarySpecs" } } var nextNodeId = 0 private set private val filesystem = Jimfs.newFileSystem(unix()) private val busyLatch = ReusableLatch() - val messagingNetwork = InMemoryMessagingNetwork(networkSendManuallyPumped, servicePeerAllocationStrategy, busyLatch) + val messagingNetwork = InMemoryMessagingNetwork.create(networkSendManuallyPumped, servicePeerAllocationStrategy, busyLatch) // A unique identifier for this network to segregate databases with the same nodeID but different networks. private val networkId = random63BitValue() - private val networkParameters: NetworkParametersCopier + private val networkParametersCopier: NetworkParametersCopier private val _nodes = mutableListOf() private val serializationEnv = try { - setGlobalSerialization(initialiseSerialization) + setGlobalSerialization(true) } catch (e: IllegalStateException) { throw IllegalStateException("Using more than one InternalMockNetwork simultaneously is not supported.", e) } @@ -199,7 +175,7 @@ open class InternalMockNetwork(private val cordappPackages: List, filesystem.getPath("/nodes").createDirectory() val notaryInfos = generateNotaryIdentities() // The network parameters must be serialised before starting any of the nodes - networkParameters = NetworkParametersCopier(testNetworkParameters(notaryInfos, maxTransactionSize = maxTransactionSize)) + networkParametersCopier = NetworkParametersCopier(networkParameters.copy(notaries = notaryInfos)) @Suppress("LeakingThis") notaryNodes = createNotaries() } catch (t: Throwable) { @@ -251,7 +227,7 @@ open class InternalMockNetwork(private val cordappPackages: List, override val started: StartedNode? get() = uncheckedCast(super.started) override fun start(): StartedNode { - mockNet.networkParameters.install(configuration.baseDirectory) + mockNet.networkParametersCopier.install(configuration.baseDirectory) val started: StartedNode = uncheckedCast(super.start()) advertiseNodeToNetwork(started) return started @@ -269,7 +245,7 @@ open class InternalMockNetwork(private val cordappPackages: List, // We only need to override the messaging service here, as currently everything that hits disk does so // through the java.nio API which we are already mocking via Jimfs. - override fun makeMessagingService(database: CordaPersistence, info: NodeInfo, nodeProperties: NodePropertiesStore): MessagingService { + override fun makeMessagingService(database: CordaPersistence, info: NodeInfo, nodeProperties: NodePropertiesStore, networkParameters: NetworkParameters): MessagingService { require(id >= 0) { "Node ID must be zero or positive, was passed: " + id } return mockNet.messagingNetwork.createNodeWithID( !mockNet.threadPerNode, @@ -311,13 +287,15 @@ open class InternalMockNetwork(private val cordappPackages: List, override fun makeTransactionVerifierService() = InMemoryTransactionVerifierService(1) - override fun myAddresses(): List = emptyList() + // NodeInfo requires a non-empty addresses list and so we give it a dummy value for mock nodes. + // The non-empty addresses check is important to have and so we tolerate the ugliness here. + override fun myAddresses(): List = listOf(NetworkHostAndPort("mock.node", 1000)) // Allow unit tests to modify the serialization whitelist list before the node start, // so they don't have to ServiceLoad test whitelists into all unit tests. - val testSerializationWhitelists by lazy { super.serializationWhitelists.toMutableList() } + private val _serializationWhitelists by lazy { super.serializationWhitelists.toMutableList() } override val serializationWhitelists: List - get() = testSerializationWhitelists + get() = _serializationWhitelists private var dbCloser: (() -> Any?)? = null override fun initialiseDatabasePersistence(schemaService: SchemaService, identityService: IdentityService, insideTransaction: (CordaPersistence) -> T): T { return super.initialiseDatabasePersistence(schemaService, identityService) { database -> @@ -467,7 +445,7 @@ private fun mockNodeConfiguration(): NodeConfiguration { doReturn(null).whenever(it).notary doReturn(DatabaseConfig()).whenever(it).database doReturn("").whenever(it).emailAddress - doReturn("").whenever(it).exportJMXto + doReturn(null).whenever(it).jmxMonitoringHttpPort doReturn(true).whenever(it).devMode doReturn(null).whenever(it).compatibilityZoneURL doReturn(emptyList()).whenever(it).certificateChainCheckPolicies diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt index 560a52a865..dd8237450b 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt @@ -53,7 +53,7 @@ abstract class NodeBasedTest(private val cordappPackages: List = emptyLi @Before fun init() { - defaultNetworkParameters = NetworkParametersCopier(testNetworkParameters(emptyList())) + defaultNetworkParameters = NetworkParametersCopier(testNetworkParameters()) } /** diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt index e2770f4b80..040f42057d 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt @@ -15,6 +15,7 @@ import net.corda.core.internal.concurrent.map import net.corda.core.internal.div import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.RPCOps +import net.corda.core.node.NetworkParameters import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.internal.security.RPCSecurityManagerImpl import net.corda.node.services.messaging.RPCServer @@ -23,6 +24,7 @@ import net.corda.nodeapi.ArtemisTcpTransport import net.corda.nodeapi.ConnectionDirection import net.corda.nodeapi.RPCApi import net.corda.nodeapi.internal.serialization.KRYO_RPC_CLIENT_CONTEXT +import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.MAX_MESSAGE_SIZE import net.corda.testing.driver.JmxPolicy import net.corda.testing.driver.PortAllocation @@ -106,7 +108,7 @@ fun rpcDriver( notarySpecs: List = emptyList(), externalTrace: Trace? = null, jmxPolicy: JmxPolicy = JmxPolicy(), - maxTransactionSize: Int = Int.MAX_VALUE, + networkParameters: NetworkParameters = testNetworkParameters(), dsl: RPCDriverDSL.() -> A ): A { return genericDriver( @@ -124,7 +126,7 @@ fun rpcDriver( notarySpecs = notarySpecs, jmxPolicy = jmxPolicy, compatibilityZone = null, - maxTransactionSize = maxTransactionSize + networkParameters = networkParameters ), externalTrace ), coerce = { it }, @@ -157,7 +159,7 @@ data class RPCDriverDSL( private val driverDSL: DriverDSLImpl, private val externalTrace: Trace? ) : InternalDriverDSL by driverDSL { private companion object { - val notificationAddress = "notifications" + const val notificationAddress = "notifications" private fun ConfigurationImpl.configureCommonSettings(maxFileSize: Int, maxBufferedBytesPerClient: Long) { managementNotificationAddress = SimpleString(notificationAddress) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/demorun/CordformNodeRunner.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/demorun/CordformNodeRunner.kt new file mode 100644 index 0000000000..bbaf5724a5 --- /dev/null +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/demorun/CordformNodeRunner.kt @@ -0,0 +1,77 @@ +package net.corda.testing.node.internal.demorun + +import net.corda.cordform.CordformDefinition +import net.corda.cordform.CordformNode +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.getOrThrow +import net.corda.testing.driver.JmxPolicy +import net.corda.testing.driver.PortAllocation +import net.corda.testing.node.internal.internalDriver + +/** + * Creates a demo runner for this cordform definition + */ +fun CordformDefinition.nodeRunner() = CordformNodeRunner(this) + +/** + * A node runner creates and runs nodes for a given [[CordformDefinition]]. + */ +class CordformNodeRunner(val cordformDefinition: CordformDefinition) { + private var extraPackagesToScan = emptyList() + + /** + * Builder method to sets the extra cordapp scan packages + */ + fun scanPackages(packages: List): CordformNodeRunner { + extraPackagesToScan = packages + return this + } + + fun clean() { + System.err.println("Deleting: ${cordformDefinition.nodesDirectory}") + cordformDefinition.nodesDirectory.toFile().deleteRecursively() + } + + /** + * Deploy the nodes specified in the given [CordformDefinition]. This will block until all the nodes and webservers + * have terminated. + */ + fun deployAndRunNodes() { + runNodes(waitForAllNodesToFinish = true) { } + } + + /** + * Deploy the nodes specified in the given [CordformDefinition] and then execute the given [block] once all the nodes + * and webservers are up. After execution all these processes will be terminated. + */ + fun deployAndRunNodesThen(block: () -> Unit) { + runNodes(waitForAllNodesToFinish = false, block = block) + } + + private fun runNodes(waitForAllNodesToFinish: Boolean, block: () -> Unit) { + clean() + val nodes = cordformDefinition.nodeConfigurers.map { configurer -> CordformNode().also { configurer.accept(it) } } + val maxPort = nodes + .flatMap { listOf(it.p2pAddress, it.rpcAddress, it.webAddress) } + .mapNotNull { address -> address?.let { NetworkHostAndPort.parse(it).port } } + .max()!! + internalDriver( + isDebug = true, + jmxPolicy = JmxPolicy(true), + driverDirectory = cordformDefinition.nodesDirectory, + extraCordappPackagesToScan = extraPackagesToScan, + // Notaries are manually specified in Cordform so we don't want the driver automatically starting any + notarySpecs = emptyList(), + // Start from after the largest port used to prevent port clash + portAllocation = PortAllocation.Incremental(maxPort + 1), + waitForAllNodesToFinish = waitForAllNodesToFinish + ) { + cordformDefinition.setup(this) + startCordformNodes(nodes).getOrThrow() // Only proceed once everything is up and running + println("All nodes and webservers are ready...") + block() + } + } +} + + diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/demorun/DemoRunner.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/demorun/DemoRunner.kt deleted file mode 100644 index 185a851e99..0000000000 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/demorun/DemoRunner.kt +++ /dev/null @@ -1,57 +0,0 @@ -@file:JvmName("DemoRunner") - -package net.corda.testing.node.internal.demorun - -import net.corda.cordform.CordformDefinition -import net.corda.cordform.CordformNode -import net.corda.core.utilities.NetworkHostAndPort -import net.corda.core.utilities.getOrThrow -import net.corda.testing.driver.JmxPolicy -import net.corda.testing.driver.PortAllocation -import net.corda.testing.node.internal.internalDriver - -fun CordformDefinition.clean() { - System.err.println("Deleting: $nodesDirectory") - nodesDirectory.toFile().deleteRecursively() -} - -/** - * Deploy the nodes specified in the given [CordformDefinition]. This will block until all the nodes and webservers - * have terminated. - */ -fun CordformDefinition.deployNodes() { - runNodes(waitForAllNodesToFinish = true) { } -} - -/** - * Deploy the nodes specified in the given [CordformDefinition] and then execute the given [block] once all the nodes - * and webservers are up. After execution all these processes will be terminated. - */ -fun CordformDefinition.deployNodesThen(block: () -> Unit) { - runNodes(waitForAllNodesToFinish = false, block = block) -} - -private fun CordformDefinition.runNodes(waitForAllNodesToFinish: Boolean, block: () -> Unit) { - clean() - val nodes = nodeConfigurers.map { configurer -> CordformNode().also { configurer.accept(it) } } - val maxPort = nodes - .flatMap { listOf(it.p2pAddress, it.rpcAddress, it.webAddress) } - .mapNotNull { address -> address?.let { NetworkHostAndPort.parse(it).port } } - .max()!! - internalDriver( - isDebug = true, - jmxPolicy = JmxPolicy(true), - driverDirectory = nodesDirectory, - extraCordappPackagesToScan = cordappPackages, - // Notaries are manually specified in Cordform so we don't want the driver automatically starting any - notarySpecs = emptyList(), - // Start from after the largest port used to prevent port clash - portAllocation = PortAllocation.Incremental(maxPort + 1), - waitForAllNodesToFinish = waitForAllNodesToFinish - ) { - setup(this) - startCordformNodes(nodes).getOrThrow() // Only proceed once everything is up and running - println("All nodes and webservers are ready...") - block() - } -} diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/network/NetworkMapServer.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/network/NetworkMapServer.kt index dbacebb931..4d0e952ce2 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/network/NetworkMapServer.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/network/NetworkMapServer.kt @@ -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 diff --git a/testing/node-driver/src/test/java/net/corda/testing/node/MockNodeFactoryInJavaTest.java b/testing/node-driver/src/test/java/net/corda/testing/node/MockNodeFactoryInJavaTest.java index fbf6bb01f4..739e5dcf27 100644 --- a/testing/node-driver/src/test/java/net/corda/testing/node/MockNodeFactoryInJavaTest.java +++ b/testing/node-driver/src/test/java/net/corda/testing/node/MockNodeFactoryInJavaTest.java @@ -13,7 +13,7 @@ public class MockNodeFactoryInJavaTest { private static void factoryIsEasyToPassInUsingJava() { //noinspection Convert2MethodRef new MockNetwork(emptyList()); - new MockNetwork(emptyList(), new MockNetworkParameters().setInitialiseSerialization(false)); + new MockNetwork(emptyList(), new MockNetworkParameters().withThreadPerNode(true)); //noinspection Convert2MethodRef new MockNetwork(emptyList()).createNode(new MockNodeParameters()); } diff --git a/testing/node-driver/src/test/kotlin/net/corda/testing/node/internal/InternalMockNetworkTests.kt b/testing/node-driver/src/test/kotlin/net/corda/testing/node/internal/InternalMockNetworkTests.kt index 478585af17..fe2780977a 100644 --- a/testing/node-driver/src/test/kotlin/net/corda/testing/node/internal/InternalMockNetworkTests.kt +++ b/testing/node-driver/src/test/kotlin/net/corda/testing/node/internal/InternalMockNetworkTests.kt @@ -9,7 +9,7 @@ class InternalMockNetworkTests { fun `does not leak serialization env if init fails`() { val e = Exception("didn't work") assertThatThrownBy { - object : InternalMockNetwork(emptyList(), initialiseSerialization = true) { + object : InternalMockNetwork(emptyList()) { override fun createNotaries() = throw e } }.isSameAs(e) diff --git a/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt index 38ebfc9d2b..11ec0e7bb7 100644 --- a/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt +++ b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt @@ -9,8 +9,8 @@ import net.corda.core.internal.div import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger import net.corda.nodeapi.internal.network.NetworkParametersCopier -import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.common.internal.asContextEnv +import net.corda.testing.common.internal.testNetworkParameters import java.net.URL import java.nio.file.Path import java.nio.file.Paths @@ -51,8 +51,8 @@ class NodeProcess( // TODO All use of this factory have duplicate code which is either bundling the calling module or a 3rd party module // as a CorDapp for the nodes. class Factory( - val buildDirectory: Path = Paths.get("build"), - val cordaJarUrl: URL? = this::class.java.getResource("/corda.jar") + private val buildDirectory: Path = Paths.get("build"), + private val cordaJarUrl: URL? = this::class.java.getResource("/corda.jar") ) { val cordaJar: Path by lazy { require(cordaJarUrl != null, { "corda.jar could not be found in classpath" }) @@ -66,7 +66,7 @@ class NodeProcess( KryoClientSerializationScheme.createSerializationEnv().asContextEnv { // There are no notaries in the network parameters for smoke test nodes. If this is required then we would // need to introduce the concept of a "network" which predefines the notaries, like the driver and MockNetwork - NetworkParametersCopier(testNetworkParameters(emptyList())) + NetworkParametersCopier(testNetworkParameters()) } } diff --git a/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/ParametersUtilities.kt b/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/ParametersUtilities.kt index ea089257c2..5f86f26b34 100644 --- a/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/ParametersUtilities.kt +++ b/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/ParametersUtilities.kt @@ -5,12 +5,12 @@ import net.corda.core.node.NotaryInfo import java.time.Instant fun testNetworkParameters( - notaries: List, + notaries: List = emptyList(), minimumPlatformVersion: Int = 1, modifiedTime: Instant = Instant.now(), maxMessageSize: Int = 10485760, // TODO: Make this configurable and consistence across driver, bootstrapper, demobench and NetworkMapServer - maxTransactionSize: Int = Int.MAX_VALUE, + maxTransactionSize: Int = maxMessageSize, epoch: Int = 1 ): NetworkParameters { return NetworkParameters( @@ -19,6 +19,7 @@ fun testNetworkParameters( modifiedTime = modifiedTime, maxMessageSize = maxMessageSize, maxTransactionSize = maxTransactionSize, - epoch = epoch + epoch = epoch, + whitelistedContractImplementations = emptyMap() ) } \ No newline at end of file diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/core/SerializationTestHelpers.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/core/SerializationTestHelpers.kt index 698003380d..2feefc269b 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/core/SerializationTestHelpers.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/core/SerializationTestHelpers.kt @@ -16,6 +16,8 @@ import net.corda.nodeapi.internal.serialization.* import net.corda.nodeapi.internal.serialization.amqp.AMQPClientSerializationScheme import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme import net.corda.testing.common.internal.asContextEnv +import net.corda.testing.internal.createTestSerializationEnv +import net.corda.testing.internal.inVMExecutors import net.corda.testing.internal.rigorousMock import net.corda.testing.internal.testThreadFactory import org.apache.activemq.artemis.core.remoting.impl.invm.InVMConnector @@ -26,8 +28,6 @@ import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ExecutorService import java.util.concurrent.Executors -private val inVMExecutors = ConcurrentHashMap() - /** @param inheritable whether new threads inherit the environment, use sparingly. */ class SerializationEnvironmentRule(private val inheritable: Boolean = false) : TestRule { companion object { @@ -78,42 +78,3 @@ interface GlobalSerializationEnvironment : SerializationEnvironment { fun unset() } -/** - * Should only be used by Driver and MockNode. - * @param armed true to install, false to do nothing and return a dummy env. - */ -fun setGlobalSerialization(armed: Boolean): GlobalSerializationEnvironment { - return if (armed) { - object : GlobalSerializationEnvironment, SerializationEnvironment by createTestSerializationEnv("") { - override fun unset() { - _globalSerializationEnv.set(null) - inVMExecutors.remove(this) - } - }.also { - _globalSerializationEnv.set(it) - } - } else { - rigorousMock().also { - doNothing().whenever(it).unset() - } - } -} - -private fun createTestSerializationEnv(label: String): SerializationEnvironmentImpl { - val factory = SerializationFactoryImpl().apply { - registerScheme(KryoClientSerializationScheme()) - registerScheme(KryoServerSerializationScheme()) - registerScheme(AMQPClientSerializationScheme(emptyList())) - registerScheme(AMQPServerSerializationScheme(emptyList())) - } - return object : SerializationEnvironmentImpl( - factory, - AMQP_P2P_CONTEXT, - KRYO_RPC_SERVER_CONTEXT, - KRYO_RPC_CLIENT_CONTEXT, - AMQP_STORAGE_CONTEXT, - KRYO_CHECKPOINT_CONTEXT - ) { - override fun toString() = "testSerializationEnv($label)" - } -} diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/core/TestConstants.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/core/TestConstants.kt index 0a87461d8d..6cbecab924 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/core/TestConstants.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/core/TestConstants.kt @@ -30,10 +30,6 @@ val BOB_NAME = CordaX500Name("Bob Plc", "Rome", "IT") @JvmField val CHARLIE_NAME = CordaX500Name("Charlie Ltd", "Athens", "GR") -val DEV_INTERMEDIATE_CA: CertificateAndKeyPair by lazy { net.corda.nodeapi.internal.DEV_INTERMEDIATE_CA } - -val DEV_ROOT_CA: CertificateAndKeyPair by lazy { net.corda.nodeapi.internal.DEV_ROOT_CA } - fun dummyCommand(vararg signers: PublicKey = arrayOf(generateKeyPair().public)) = Command(DummyCommandData, signers.toList()) object DummyCommandData : TypeOnlyCommandData() diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/core/TestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/core/TestUtils.kt index 762ea036be..5229cde993 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/core/TestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/core/TestUtils.kt @@ -20,6 +20,8 @@ import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509CertificateFactory import net.corda.nodeapi.internal.crypto.X509Utilities +import net.corda.testing.internal.DEV_INTERMEDIATE_CA +import net.corda.testing.internal.DEV_ROOT_CA import java.math.BigInteger import java.security.KeyPair import java.security.PublicKey diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt index 8548ecc1fb..24ba8921c8 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt @@ -14,7 +14,7 @@ import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction import net.corda.testing.services.MockAttachmentStorage -import net.corda.testing.services.MockCordappProvider +import net.corda.testing.internal.MockCordappProvider import net.corda.testing.core.dummyCommand import java.io.InputStream import java.security.PublicKey @@ -77,6 +77,9 @@ data class TestTransactionDSLInterpreter private constructor( val services = object : ServicesForResolution by ledgerInterpreter.services { override fun loadState(stateRef: StateRef) = ledgerInterpreter.resolveStateRef(stateRef) + override fun loadStates(stateRefs: Set): Set> { + return stateRefs.map { StateAndRef(loadState(it), it) }.toSet() + } override val cordappProvider: CordappProvider = ledgerInterpreter.services.cordappProvider } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalSerializationTestHelpers.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalSerializationTestHelpers.kt index f11693d527..aaa9e3a15d 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalSerializationTestHelpers.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalSerializationTestHelpers.kt @@ -1,8 +1,19 @@ package net.corda.testing.internal -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.internal._inheritableContextSerializationEnv +import com.nhaarman.mockito_kotlin.doNothing +import com.nhaarman.mockito_kotlin.whenever +import net.corda.client.rpc.internal.KryoClientSerializationScheme +import net.corda.core.serialization.internal.* +import net.corda.node.serialization.KryoServerSerializationScheme +import net.corda.nodeapi.internal.serialization.* +import net.corda.nodeapi.internal.serialization.amqp.AMQPClientSerializationScheme +import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme +import net.corda.testing.core.GlobalSerializationEnvironment import net.corda.testing.core.SerializationEnvironmentRule +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ExecutorService + +val inVMExecutors = ConcurrentHashMap() /** * For example your test class uses [SerializationEnvironmentRule] but you want to turn it off for one method. @@ -17,3 +28,43 @@ fun withoutTestSerialization(callable: () -> T): T { // TODO: Delete this, s property.set(env) } } + +internal fun createTestSerializationEnv(label: String): SerializationEnvironmentImpl { + val factory = SerializationFactoryImpl().apply { + registerScheme(KryoClientSerializationScheme()) + registerScheme(KryoServerSerializationScheme()) + registerScheme(AMQPClientSerializationScheme(emptyList())) + registerScheme(AMQPServerSerializationScheme(emptyList())) + } + return object : SerializationEnvironmentImpl( + factory, + AMQP_P2P_CONTEXT, + KRYO_RPC_SERVER_CONTEXT, + KRYO_RPC_CLIENT_CONTEXT, + AMQP_STORAGE_CONTEXT, + KRYO_CHECKPOINT_CONTEXT + ) { + override fun toString() = "testSerializationEnv($label)" + } +} + +/** + * Should only be used by Driver and MockNode. + * @param armed true to install, false to do nothing and return a dummy env. + */ +fun setGlobalSerialization(armed: Boolean): GlobalSerializationEnvironment { + return if (armed) { + object : GlobalSerializationEnvironment, SerializationEnvironment by createTestSerializationEnv("") { + override fun unset() { + _globalSerializationEnv.set(null) + inVMExecutors.remove(this) + } + }.also { + _globalSerializationEnv.set(it) + } + } else { + rigorousMock().also { + doNothing().whenever(it).unset() + } + } +} \ No newline at end of file diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestConstants.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestConstants.kt new file mode 100644 index 0000000000..d9165951a8 --- /dev/null +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestConstants.kt @@ -0,0 +1,7 @@ +package net.corda.testing.internal + +import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair + +val DEV_INTERMEDIATE_CA: CertificateAndKeyPair by lazy { net.corda.nodeapi.internal.DEV_INTERMEDIATE_CA } + +val DEV_ROOT_CA: CertificateAndKeyPair by lazy { net.corda.nodeapi.internal.DEV_ROOT_CA } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappConfigProvider.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappConfigProvider.kt new file mode 100644 index 0000000000..6f2f99cfa8 --- /dev/null +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappConfigProvider.kt @@ -0,0 +1,17 @@ +package net.corda.testing.internal + +import com.typesafe.config.Config +import com.typesafe.config.ConfigFactory +import net.corda.core.internal.cordapp.CordappConfigProvider + +class MockCordappConfigProvider : CordappConfigProvider { + val cordappConfigs = mutableMapOf () + + override fun getConfigByName(name: String): Config { + return if(cordappConfigs.containsKey(name)) { + cordappConfigs[name]!! + } else { + ConfigFactory.empty() + } + } +} \ No newline at end of file diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockCordappProvider.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt similarity index 53% rename from testing/test-utils/src/main/kotlin/net/corda/testing/services/MockCordappProvider.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt index dba07b73be..e04b2aa580 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockCordappProvider.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt @@ -1,16 +1,25 @@ -package net.corda.testing.services +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 import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl +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>, + cordappConfigProvider: MockCordappConfigProvider = MockCordappConfigProvider() +) : CordappProviderImpl(cordappLoader, cordappConfigProvider, attachmentStorage, whitelistedContractImplementations) { + constructor(cordappLoader: CordappLoader, attachmentStorage: AttachmentStorage, whitelistedContractImplementations: Map>) : this(cordappLoader, attachmentStorage, whitelistedContractImplementations, MockCordappConfigProvider()) + val cordappRegistry = mutableListOf>() fun addMockCordapp(contractClassName: ContractClassName, attachments: MockAttachmentStorage) { @@ -24,22 +33,23 @@ class MockCordappProvider(cordappLoader: CordappLoader, attachmentStorage: Attac serializationWhitelists = emptyList(), serializationCustomSerializers = emptyList(), customSchemas = emptySet(), - jarPath = Paths.get(".").toUri().toURL()) + 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, 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()) } } } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt index c99ca190cf..8757adde2b 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt @@ -12,8 +12,6 @@ import net.corda.nodeapi.internal.createDevNodeCa import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509Utilities -import net.corda.testing.core.DEV_INTERMEDIATE_CA -import net.corda.testing.core.DEV_ROOT_CA import java.security.KeyPair import java.security.PrivateKey import java.security.cert.X509Certificate diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt index 8cf1a9ccf2..dc96dfcd7c 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt @@ -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,50 +28,50 @@ 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>() - 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 - } - return sha256 - } - - override fun importAttachment(jar: InputStream, uploader: String, filename: String): AttachmentId { - return importAttachment(jar) - } - - val files = HashMap() - - override fun openAttachment(id: SecureHash): Attachment? { - val f = files[id] ?: return null - return object : AbstractAttachment({ f }) { - override val id = id + override fun importAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId { + return withContractsInJar(jar) { contractClassNames, inputStream -> + importAttachmentInternal(inputStream, uploader, filename, contractClassNames) } } + override fun openAttachment(id: SecureHash): Attachment? = files[id]?.first + override fun queryAttachments(criteria: AttachmentQueryCriteria, sorting: AttachmentSort?): List { throw NotImplementedError("Querying for attachments not implemented") } override fun hasAttachment(attachmentId: AttachmentId) = files.containsKey(attachmentId) - fun getAttachmentIdAndBytes(jar: InputStream): Pair { - 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, uploader: String, jar: InputStream): AttachmentId = importAttachmentInternal(jar, uploader, null, contractClassNames) + + fun getAttachmentIdAndBytes(jar: InputStream): Pair = 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? = 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 + } } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt index ae5b6deeb1..b251955d5c 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt @@ -1,6 +1,9 @@ package net.corda.demobench.model +import com.typesafe.config.Config +import com.typesafe.config.ConfigFactory.empty import com.typesafe.config.ConfigRenderOptions +import com.typesafe.config.ConfigValueFactory import net.corda.core.identity.CordaX500Name import net.corda.core.internal.copyToDirectory import net.corda.core.internal.createDirectories @@ -19,6 +22,7 @@ data class NodeConfig( val myLegalName: CordaX500Name, val p2pAddress: NetworkHostAndPort, val rpcAddress: NetworkHostAndPort, + val rpcAdminAddress: NetworkHostAndPort, /** This is not used by the node but by the webserver which looks at node.conf. */ val webAddress: NetworkHostAndPort, val notary: NotaryService?, @@ -38,7 +42,17 @@ data class NodeConfig( @Suppress("unused") private val useTestClock = true - fun toText(): String = toConfig().root().render(renderOptions) + private fun asConfig(): Config { + + val config = toConfig() + val rpcSettings = empty() + .withValue("address", ConfigValueFactory.fromAnyRef(rpcAddress.toString())) + .withValue("adminAddress", ConfigValueFactory.fromAnyRef(rpcAdminAddress.toString())) + .root() + return config.withoutPath("rpcAddress").withoutPath("rpcAdminAddress").withValue("rpcSettings", rpcSettings) + } + + fun toText(): String = asConfig().root().render(renderOptions) } /** diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt index 1c53fdd82f..e96c4b847d 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt @@ -75,6 +75,7 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { ), p2pAddress = nodeData.p2pPort.toLocalAddress(), rpcAddress = nodeData.rpcPort.toLocalAddress(), + rpcAdminAddress = nodeData.rpcAdminPort.toLocalAddress(), webAddress = nodeData.webPort.toLocalAddress(), notary = notary, h2port = nodeData.h2Port.value, @@ -144,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 diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeData.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeData.kt index 7abdd09775..b8af55e5ff 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeData.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeData.kt @@ -35,6 +35,7 @@ class NodeData { val nearestCity = SimpleObjectProperty(CityDatabase["London"]!!) val p2pPort = SimpleIntegerProperty() val rpcPort = SimpleIntegerProperty() + val rpcAdminPort = SimpleIntegerProperty() val webPort = SimpleIntegerProperty() val h2Port = SimpleIntegerProperty() val extraServices = SimpleListProperty(observableArrayList()) @@ -45,6 +46,7 @@ class NodeDataModel : ItemViewModel(NodeData()) { val nearestCity = bind { item?.nearestCity } val p2pPort = bind { item?.p2pPort } val rpcPort = bind { item?.rpcPort } + val rpcAdminPort = bind { item?.rpcAdminPort } val webPort = bind { item?.webPort } val h2Port = bind { item?.h2Port } } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt index 5fb32aa9e6..8cded0186d 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt @@ -188,6 +188,7 @@ class NodeTabView : Fragment() { model.p2pPort.value = nodeController.nextPort model.rpcPort.value = nodeController.nextPort + model.rpcAdminPort.value = nodeController.nextPort model.webPort.value = nodeController.nextPort model.h2Port.value = nodeController.nextPort diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt index 1c82a08654..cbe6f54a18 100644 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt @@ -28,6 +28,7 @@ class NodeConfigTest { legalName = myLegalName, p2pPort = 10001, rpcPort = 40002, + rpcAdminPort = 40003, webPort = 20001, h2port = 30001, notary = NotaryService(validating = false), @@ -55,6 +56,7 @@ class NodeConfigTest { legalName = myLegalName, p2pPort = 10001, rpcPort = 40002, + rpcAdminPort = 40003, webPort = 20001, h2port = 30001, notary = NotaryService(validating = false), @@ -77,6 +79,7 @@ class NodeConfigTest { legalName: CordaX500Name = CordaX500Name(organisation = "Unknown", locality = "Nowhere", country = "GB"), p2pPort: Int = -1, rpcPort: Int = -1, + rpcAdminPort: Int = -1, webPort: Int = -1, h2port: Int = -1, notary: NotaryService?, @@ -86,6 +89,7 @@ class NodeConfigTest { myLegalName = legalName, p2pAddress = localPort(p2pPort), rpcAddress = localPort(rpcPort), + rpcAdminAddress = localPort(rpcAdminPort), webAddress = localPort(webPort), h2port = h2port, notary = notary, diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt index 18db310a5b..e6b5454144 100644 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt @@ -150,6 +150,7 @@ class NodeControllerTest { organisation: String = "Unknown", p2pPort: Int = 0, rpcPort: Int = 0, + rpcAdminPort: Int = 0, webPort: Int = 0, h2port: Int = 0, notary: NotaryService? = null, @@ -163,6 +164,7 @@ class NodeControllerTest { ), p2pAddress = localPort(p2pPort), rpcAddress = localPort(rpcPort), + rpcAdminAddress = localPort(rpcAdminPort), webAddress = localPort(webPort), h2port = h2port, notary = notary, diff --git a/tools/scripts/upgrade-test-packages.ps1 b/tools/scripts/upgrade-test-packages.ps1 new file mode 100644 index 0000000000..79028809a2 --- /dev/null +++ b/tools/scripts/upgrade-test-packages.ps1 @@ -0,0 +1,4 @@ +Get-ChildItem $args[0] -Include *.kt, *.java -Recurse | +Foreach-Object { + (Get-Content $_.FullName) -replace 'net.corda.testing.(\*|generateStateRef|freeLocalHostAndPort|getFreeLocalPorts|getTestPartyAndCertificate|TestIdentity|chooseIdentity|singleIdentity|TEST_TX_TIME|DUMMY_NOTARY_NAME|DUMMY_BANK_A_NAME|DUMMY_BANK_B_NAME|DUMMY_BANK_C_NAME|BOC_NAME|ALICE_NAME|BOB_NAME|CHARLIE_NAME|DEV_INTERMEDIATE_CA|DEV_ROOT_CA|dummyCommand|DummyCommandData|MAX_MESSAGE_SIZE|SerializationEnvironmentRule|setGlobalSerialization|expect|sequence|parallel|replicate|genericExpectEvents)', 'net.corda.testing.core.$1' -replace 'net.corda.testing.FlowStackSnapshotFactoryImpl', 'net.corda.testing.services.FlowStackSnapshotFactoryImpl' -join "`r`n" | Set-Content -NoNewline -Force $_.FullName +} \ No newline at end of file diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt index afc019ed9a..68c85a65bf 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt @@ -1,17 +1,27 @@ package net.corda.verifier +import com.nhaarman.mockito_kotlin.any +import com.nhaarman.mockito_kotlin.doAnswer +import com.nhaarman.mockito_kotlin.whenever import net.corda.client.mock.Generator import net.corda.core.contracts.* +import net.corda.core.cordapp.CordappProvider import net.corda.core.crypto.SecureHash import net.corda.core.crypto.entropyToKeyPair import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party +import net.corda.core.node.NetworkParameters +import net.corda.core.node.ServicesForResolution +import net.corda.core.node.services.AttachmentStorage +import net.corda.core.node.services.IdentityService import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.WireTransaction import net.corda.nodeapi.internal.serialization.GeneratedAttachment +import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.contracts.DummyContract +import net.corda.testing.internal.rigorousMock import org.apache.commons.lang.ArrayUtils.EMPTY_BYTE_ARRAY import java.math.BigInteger import java.security.PublicKey @@ -29,11 +39,34 @@ data class GeneratedLedger( val attachments: Set, val identities: Set ) { - val hashTransactionMap: Map by lazy { transactions.associateBy(WireTransaction::id) } - val attachmentMap: Map by lazy { attachments.associateBy(Attachment::id) } - val identityMap: Map by lazy { identities.associateBy(Party::owningKey) } - val contractAttachmentMap: Map by lazy { - attachments.mapNotNull { it as? ContractAttachment }.associateBy { it.contract } + private val hashTransactionMap: Map by lazy { transactions.associateBy(WireTransaction::id) } + private val attachmentMap: Map by lazy { attachments.associateBy(Attachment::id) } + private val identityMap: Map by lazy { identities.associateBy(Party::owningKey) } + private val contractAttachmentMap: Map by lazy { + attachments.mapNotNull { it as? ContractAttachment }.flatMap { attch-> attch.allContracts.map { it to attch } }.toMap() + } + + private val services = object : ServicesForResolution { + override fun loadState(stateRef: StateRef): TransactionState<*> { + return hashTransactionMap[stateRef.txhash]?.outputs?.get(stateRef.index) ?: throw TransactionResolutionException(stateRef.txhash) + } + + override fun loadStates(stateRefs: Set): Set> { + return stateRefs.groupBy { it.txhash }.flatMap { + val outputs = hashTransactionMap[it.key]?.outputs ?: throw TransactionResolutionException(it.key) + it.value.map { StateAndRef(outputs[it.index], it) } + }.toSet() + } + override val identityService = rigorousMock().apply { + doAnswer { identityMap[it.arguments[0]] }.whenever(this).partyFromKey(any()) + } + override val attachments = rigorousMock().apply { + doAnswer { attachmentMap[it.arguments[0]] }.whenever(this).openAttachment(any()) + } + override val cordappProvider = rigorousMock().apply { + doAnswer { contractAttachmentMap[it.arguments[0]]?.id }.whenever(this).getContractAttachmentID(any()) + } + override val networkParameters = testNetworkParameters() } companion object { @@ -42,12 +75,7 @@ data class GeneratedLedger( } fun resolveWireTransaction(transaction: WireTransaction): LedgerTransaction { - return transaction.toLedgerTransaction( - resolveIdentity = { identityMap[it] }, - resolveAttachment = { attachmentMap[it] }, - resolveStateRef = { hashTransactionMap[it.txhash]?.outputs?.get(it.index) }, - resolveContractAttachment = { contractAttachmentMap[it.contract]?.id } - ) + return transaction.toLedgerTransaction(services) } val attachmentsGenerator: Generator> by lazy { diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt index 50c1b8d7e1..d9c6fcef06 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt @@ -5,13 +5,13 @@ import com.typesafe.config.ConfigFactory import net.corda.core.concurrent.CordaFuture import net.corda.core.crypto.random63BitValue import net.corda.core.identity.CordaX500Name -import net.corda.core.internal.GlobalProperties import net.corda.core.internal.concurrent.OpenFuture import net.corda.core.internal.concurrent.doneFuture import net.corda.core.internal.concurrent.fork import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.createDirectories import net.corda.core.internal.div +import net.corda.core.node.NetworkParameters import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.transactions.LedgerTransaction import net.corda.core.utilities.NetworkHostAndPort @@ -64,7 +64,7 @@ fun verifierDriver( extraCordappPackagesToScan: List = emptyList(), notarySpecs: List = emptyList(), jmxPolicy: JmxPolicy = JmxPolicy(), - maxTransactionSize: Int = Int.MAX_VALUE, + networkParameters: NetworkParameters = testNetworkParameters(), dsl: VerifierDriverDSL.() -> A ) = genericDriver( driverDsl = VerifierDriverDSL( @@ -81,7 +81,7 @@ fun verifierDriver( notarySpecs = notarySpecs, jmxPolicy = jmxPolicy, compatibilityZone = null, - maxTransactionSize = maxTransactionSize + networkParameters = networkParameters ) ), coerce = { it }, @@ -167,7 +167,6 @@ data class VerifierDriverDSL(private val driverDSL: DriverDSLImpl) : InternalDri /** Starts a lightweight verification requestor that implements the Node's Verifier API */ fun startVerificationRequestor(name: CordaX500Name): CordaFuture { val hostAndPort = driverDSL.portAllocation.nextHostAndPort() - GlobalProperties.networkParameters = testNetworkParameters(emptyList(), maxTransactionSize = driverDSL.maxTransactionSize) return driverDSL.executorService.fork { startVerificationRequestorInternal(name, hostAndPort) } diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt index 6de9c38700..16b3d138a2 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt @@ -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( diff --git a/webserver/build.gradle b/webserver/build.gradle index e0f66a360c..3715cfaf67 100644 --- a/webserver/build.gradle +++ b/webserver/build.gradle @@ -38,7 +38,6 @@ dependencies { compile "org.eclipse.jetty:jetty-servlet:$jetty_version" compile "org.eclipse.jetty:jetty-webapp:$jetty_version" compile "javax.servlet:javax.servlet-api:3.1.0" - compile "org.jolokia:jolokia-war:$jolokia_version" compile "commons-fileupload:commons-fileupload:$fileupload_version" // Log4J: logging framework (with SLF4J bindings) diff --git a/webserver/src/main/kotlin/net/corda/webserver/WebServerConfig.kt b/webserver/src/main/kotlin/net/corda/webserver/WebServerConfig.kt index 1f74da48ac..28a960a224 100644 --- a/webserver/src/main/kotlin/net/corda/webserver/WebServerConfig.kt +++ b/webserver/src/main/kotlin/net/corda/webserver/WebServerConfig.kt @@ -5,6 +5,7 @@ import net.corda.core.utilities.NetworkHostAndPort import net.corda.nodeapi.internal.config.NodeSSLConfiguration import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.getValue +import net.corda.nodeapi.internal.config.parseAs import java.nio.file.Path /** @@ -13,7 +14,6 @@ import java.nio.file.Path class WebServerConfig(override val baseDirectory: Path, val config: Config) : NodeSSLConfiguration { override val keyStorePassword: String by config override val trustStorePassword: String by config - val exportJMXto: String get() = "http" val useHTTPS: Boolean by config val myLegalName: String by config val rpcAddress: NetworkHostAndPort by lazy { @@ -26,5 +26,16 @@ class WebServerConfig(override val baseDirectory: Path, val config: Config) : No throw Exception("Missing rpc address property. Either 'rpcSettings' or 'rpcAddress' must be specified.") } val webAddress: NetworkHostAndPort by config - val rpcUsers: List by config + val runAs: User + + init { + // TODO: replace with credentials supplied by a user + val users = if (config.hasPath("rpcUsers")) { + // TODO: remove this once config format is updated + config.getConfigList("rpcUsers") + } else { + config.getConfigList("security.authService.dataSource.users") + } + runAs = users.first().parseAs() + } } diff --git a/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt b/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt index 4938ca99c1..958c540f61 100644 --- a/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt +++ b/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt @@ -189,10 +189,9 @@ class NodeWebServer(val config: WebServerConfig) { } private fun connectLocalRpcAsNodeUser(): CordaRPCOps { - val rpcUser = config.rpcUsers.firstOrNull() ?: throw IllegalArgumentException("The node config has not specified any RPC users") - log.info("Connecting to node at ${config.rpcAddress} as $rpcUser") + log.info("Connecting to node at ${config.rpcAddress} as ${config.runAs}") val client = CordaRPCClient(config.rpcAddress) - val connection = client.start(rpcUser.username, rpcUser.password) + val connection = client.start(config.runAs.username, config.runAs.password) return connection.proxy }