diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 12a44713fc..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() @@ -856,9 +867,6 @@ public final class net.corda.core.crypto.CryptoUtils extends java.lang.Object public final boolean verify(byte[]) @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.DigitalSignature withoutKey() ## -public final class net.corda.core.crypto.DummySecureRandom extends java.security.SecureRandom - public static final net.corda.core.crypto.DummySecureRandom INSTANCE -## public abstract class net.corda.core.crypto.MerkleTree extends java.lang.Object @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.SecureHash getHash() public static final net.corda.core.crypto.MerkleTree$Companion Companion @@ -1175,14 +1183,11 @@ public static final class net.corda.core.flows.FinalityFlow$Companion extends ja @org.jetbrains.annotations.NotNull public net.corda.core.utilities.ProgressTracker childProgressTracker() public static final net.corda.core.flows.FinalityFlow$Companion$NOTARISING INSTANCE ## -@net.corda.core.serialization.CordaSerializable public class net.corda.core.flows.FlowException extends net.corda.core.CordaException implements net.corda.core.flows.IdentifiableException +@net.corda.core.serialization.CordaSerializable public class net.corda.core.flows.FlowException extends net.corda.core.CordaException public () public (String) public (String, Throwable) public (Throwable) - @org.jetbrains.annotations.Nullable public Long getErrorId() - @org.jetbrains.annotations.Nullable public final Long getOriginalErrorId() - public final void setOriginalErrorId(Long) ## @net.corda.core.serialization.CordaSerializable public final class net.corda.core.flows.FlowInfo extends java.lang.Object public (int, String) @@ -1248,6 +1253,7 @@ public abstract class net.corda.core.flows.FlowLogic extends java.lang.Object public final void checkFlowPermission(String, Map) @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.Nullable public final net.corda.core.flows.FlowStackSnapshot flowStackSnapshot() @org.jetbrains.annotations.Nullable public static final net.corda.core.flows.FlowLogic getCurrentTopLevel() + @kotlin.Deprecated @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowInfo getFlowInfo(net.corda.core.identity.Party) @org.jetbrains.annotations.NotNull public final org.slf4j.Logger getLogger() @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party getOurIdentity() @org.jetbrains.annotations.NotNull public final net.corda.core.identity.PartyAndCertificate getOurIdentityAndCert() @@ -1256,13 +1262,13 @@ public abstract class net.corda.core.flows.FlowLogic extends java.lang.Object @org.jetbrains.annotations.NotNull public final net.corda.core.node.ServiceHub getServiceHub() @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 List receiveAll(Class, List, boolean) @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public Map receiveAllMap(Map) - @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public Map receiveAllMap(Map, boolean) 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) @co.paralleluniverse.fibers.Suspendable @kotlin.jvm.JvmStatic public static final void sleep(java.time.Duration) - @co.paralleluniverse.fibers.Suspendable @kotlin.jvm.JvmStatic public static final void sleep(java.time.Duration, boolean) @co.paralleluniverse.fibers.Suspendable public Object subFlow(net.corda.core.flows.FlowLogic) @org.jetbrains.annotations.Nullable public final net.corda.core.messaging.DataFeed track() @org.jetbrains.annotations.Nullable public final net.corda.core.messaging.DataFeed trackStepsTree() @@ -1274,7 +1280,6 @@ public abstract class net.corda.core.flows.FlowLogic extends java.lang.Object public static final class net.corda.core.flows.FlowLogic$Companion extends java.lang.Object @org.jetbrains.annotations.Nullable public final net.corda.core.flows.FlowLogic getCurrentTopLevel() @co.paralleluniverse.fibers.Suspendable @kotlin.jvm.JvmStatic public final void sleep(java.time.Duration) - @co.paralleluniverse.fibers.Suspendable @kotlin.jvm.JvmStatic public final void sleep(java.time.Duration, boolean) ## @net.corda.core.serialization.CordaSerializable @net.corda.core.DoNotImplement public interface net.corda.core.flows.FlowLogicRef ## @@ -1317,9 +1322,6 @@ public static final class net.corda.core.flows.FlowStackSnapshot$Frame extends j public int hashCode() @org.jetbrains.annotations.NotNull public String toString() ## -public interface net.corda.core.flows.IdentifiableException - @javax.annotation.Nullable public Long getErrorId() -## @net.corda.core.serialization.CordaSerializable public final class net.corda.core.flows.IllegalFlowLogicException extends java.lang.IllegalArgumentException public (Class, String) ## @@ -1543,10 +1545,9 @@ public final class net.corda.core.flows.TransactionParts extends java.lang.Objec public int hashCode() public String toString() ## -@net.corda.core.serialization.CordaSerializable public final class net.corda.core.flows.UnexpectedFlowEndException extends net.corda.core.CordaRuntimeException implements net.corda.core.flows.IdentifiableException - public (String, Throwable, long) - @org.jetbrains.annotations.NotNull public Long getErrorId() - public final long getOriginalErrorId() +@net.corda.core.serialization.CordaSerializable public final class net.corda.core.flows.UnexpectedFlowEndException extends net.corda.core.CordaRuntimeException + public (String) + public (String, Throwable) ## @net.corda.core.serialization.CordaSerializable @net.corda.core.DoNotImplement public abstract class net.corda.core.identity.AbstractParty extends java.lang.Object public (java.security.PublicKey) @@ -1641,7 +1642,6 @@ public final class net.corda.core.identity.IdentityUtils extends java.lang.Objec @kotlin.Deprecated @net.corda.core.messaging.RPCReturnsObservables @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed internalVerifiedTransactionsFeed() @kotlin.Deprecated @org.jetbrains.annotations.NotNull public abstract List internalVerifiedTransactionsSnapshot() public abstract boolean isFlowsDrainingModeEnabled() - public abstract boolean killFlow(net.corda.core.flows.StateMachineRunId) @net.corda.core.messaging.RPCReturnsObservables @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed networkMapFeed() @org.jetbrains.annotations.NotNull public abstract List networkMapSnapshot() @net.corda.core.messaging.RPCReturnsObservables @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed networkParametersFeed() @@ -1817,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() @@ -1832,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() ## @@ -1909,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) ## @@ -2055,7 +2057,6 @@ public final class net.corda.core.node.services.TimeWindowChecker extends java.l @org.jetbrains.annotations.NotNull public abstract rx.Observable getUpdates() @org.jetbrains.annotations.NotNull public abstract net.corda.core.contracts.TransactionState loadState(net.corda.core.contracts.StateRef) @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed track() - @org.jetbrains.annotations.NotNull public abstract net.corda.core.concurrent.CordaFuture trackTransaction(net.corda.core.crypto.SecureHash) ## @net.corda.core.DoNotImplement public interface net.corda.core.node.services.TransactionVerifierService @org.jetbrains.annotations.NotNull public abstract net.corda.core.concurrent.CordaFuture verify(net.corda.core.transactions.LedgerTransaction) @@ -2788,7 +2789,6 @@ public final class net.corda.core.schemas.CommonSchema extends java.lang.Object public static final net.corda.core.schemas.CommonSchema INSTANCE ## public final class net.corda.core.schemas.CommonSchemaV1 extends net.corda.core.schemas.MappedSchema - @org.jetbrains.annotations.NotNull protected String getMigrationResource() public static final net.corda.core.schemas.CommonSchemaV1 INSTANCE ## @javax.persistence.MappedSuperclass @net.corda.core.serialization.CordaSerializable public static class net.corda.core.schemas.CommonSchemaV1$FungibleState extends net.corda.core.schemas.PersistentState @@ -2819,7 +2819,6 @@ public final class net.corda.core.schemas.CommonSchemaV1 extends net.corda.core. public class net.corda.core.schemas.MappedSchema extends java.lang.Object public (Class, int, Iterable) @org.jetbrains.annotations.NotNull public final Iterable getMappedTypes() - @org.jetbrains.annotations.Nullable protected String getMigrationResource() @org.jetbrains.annotations.NotNull public final String getName() public final int getVersion() @org.jetbrains.annotations.NotNull public String toString() @@ -2891,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() @@ -3042,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() @@ -3052,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) @@ -3488,10 +3489,6 @@ public static final class net.corda.core.utilities.OpaqueBytes$Companion extends public interface net.corda.core.utilities.PropertyDelegate public abstract Object getValue(Object, kotlin.reflect.KProperty) ## -public final class net.corda.core.utilities.SgxSupport extends java.lang.Object - public static final boolean isInsideEnclave() - public static final net.corda.core.utilities.SgxSupport INSTANCE -## @net.corda.core.serialization.CordaSerializable public abstract class net.corda.core.utilities.Try extends java.lang.Object @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.Try combine(net.corda.core.utilities.Try, kotlin.jvm.functions.Function2) @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.Try flatMap(kotlin.jvm.functions.Function1) @@ -3538,7 +3535,6 @@ public static interface net.corda.core.utilities.UntrustworthyData$Validator ext @co.paralleluniverse.fibers.Suspendable public abstract Object validate(Object) ## public final class net.corda.core.utilities.UntrustworthyDataKt extends java.lang.Object - @org.jetbrains.annotations.NotNull public static final net.corda.core.utilities.UntrustworthyData checkPayloadIs(net.corda.core.serialization.SerializedBytes, Class) public static final Object unwrap(net.corda.core.utilities.UntrustworthyData, kotlin.jvm.functions.Function1) ## public final class net.corda.core.utilities.UuidGenerator extends java.lang.Object @@ -3735,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, net.corda.core.node.NetworkParameters) + 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() - @org.jetbrains.annotations.NotNull public final net.corda.core.node.NetworkParameters 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() @@ -3748,13 +3743,12 @@ 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, net.corda.core.node.NetworkParameters) + @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() @org.jetbrains.annotations.NotNull public final net.corda.core.node.NetworkParameters getNetworkParameters() @org.jetbrains.annotations.NotNull public final List getNotarySpecs() @@ -3769,7 +3763,6 @@ public final class net.corda.testing.driver.DriverParameters extends java.lang.O @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 withInitialiseSerialization(boolean) @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) @@ -3807,18 +3800,16 @@ public final class net.corda.testing.driver.JmxPolicy extends java.lang.Object ## public final class net.corda.testing.driver.NodeParameters extends java.lang.Object public () - public (net.corda.core.identity.CordaX500Name, List, net.corda.node.services.config.VerifierType, Map, Boolean, String, String) + public (net.corda.core.identity.CordaX500Name, List, net.corda.node.services.config.VerifierType, Map, Boolean, String) @org.jetbrains.annotations.Nullable public final net.corda.core.identity.CordaX500Name component1() @org.jetbrains.annotations.NotNull public final List component2() @org.jetbrains.annotations.NotNull public final net.corda.node.services.config.VerifierType component3() @org.jetbrains.annotations.NotNull public final Map component4() @org.jetbrains.annotations.Nullable public final Boolean component5() @org.jetbrains.annotations.NotNull public final String component6() - @org.jetbrains.annotations.Nullable public final String component7() - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.NodeParameters copy(net.corda.core.identity.CordaX500Name, List, net.corda.node.services.config.VerifierType, Map, Boolean, String, String) + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.NodeParameters copy(net.corda.core.identity.CordaX500Name, List, net.corda.node.services.config.VerifierType, Map, Boolean, String) public boolean equals(Object) @org.jetbrains.annotations.NotNull public final Map getCustomOverrides() - @org.jetbrains.annotations.Nullable public final String getLogLevel() @org.jetbrains.annotations.NotNull public final String getMaximumHeapSize() @org.jetbrains.annotations.Nullable public final net.corda.core.identity.CordaX500Name getProvidedName() @org.jetbrains.annotations.NotNull public final List getRpcUsers() @@ -3827,7 +3818,6 @@ public final class net.corda.testing.driver.NodeParameters extends java.lang.Obj public int hashCode() 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 withLogLevel(String) @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) @@ -3898,32 +3888,25 @@ public static final class net.corda.testing.node.ClusterSpec$Raft extends net.co public static final class net.corda.testing.node.InMemoryMessagingNetwork$Companion extends java.lang.Object ## public static final class net.corda.testing.node.InMemoryMessagingNetwork$InMemoryMessage extends java.lang.Object implements net.corda.node.services.messaging.Message - public (String, net.corda.core.utilities.ByteSequence, net.corda.node.services.statemachine.DeduplicationId, java.time.Instant, String) + public (String, net.corda.core.utilities.ByteSequence, String, java.time.Instant) @org.jetbrains.annotations.NotNull public final String component1() @org.jetbrains.annotations.NotNull public final net.corda.core.utilities.ByteSequence component2() - @org.jetbrains.annotations.NotNull public final net.corda.node.services.statemachine.DeduplicationId component3() + @org.jetbrains.annotations.NotNull public final String component3() @org.jetbrains.annotations.NotNull public final java.time.Instant component4() - @org.jetbrains.annotations.Nullable public final String component5() - @org.jetbrains.annotations.NotNull public final net.corda.testing.node.InMemoryMessagingNetwork$InMemoryMessage copy(String, net.corda.core.utilities.ByteSequence, net.corda.node.services.statemachine.DeduplicationId, java.time.Instant, String) + @org.jetbrains.annotations.NotNull public final net.corda.testing.node.InMemoryMessagingNetwork$InMemoryMessage copy(String, net.corda.core.utilities.ByteSequence, String, java.time.Instant) public boolean equals(Object) - @org.jetbrains.annotations.NotNull public Map getAdditionalHeaders() @org.jetbrains.annotations.NotNull public net.corda.core.utilities.ByteSequence getData() @org.jetbrains.annotations.NotNull public java.time.Instant getDebugTimestamp() - @org.jetbrains.annotations.Nullable public String getSenderUUID() @org.jetbrains.annotations.NotNull public String getTopic() - @org.jetbrains.annotations.NotNull public net.corda.node.services.statemachine.DeduplicationId getUniqueMessageId() + @org.jetbrains.annotations.NotNull public String getUniqueMessageId() public int hashCode() @org.jetbrains.annotations.NotNull public String toString() ## public final class net.corda.testing.node.InMemoryMessagingNetwork$InMemoryMessaging$Handler extends java.lang.Object implements net.corda.node.services.messaging.MessageHandlerRegistration - public (net.corda.testing.node.InMemoryMessagingNetwork$InMemoryMessaging, String, kotlin.jvm.functions.Function3) - @org.jetbrains.annotations.NotNull public final kotlin.jvm.functions.Function3 getCallback() + public (net.corda.testing.node.InMemoryMessagingNetwork$InMemoryMessaging, String, kotlin.jvm.functions.Function2) + @org.jetbrains.annotations.NotNull public final kotlin.jvm.functions.Function2 getCallback() @org.jetbrains.annotations.NotNull public final String getTopicSession() ## -public static final class net.corda.testing.node.InMemoryMessagingNetwork$InMemoryMessaging$pumpReceiveInternal$1$1$acknowledgeHandle$1 extends java.lang.Object implements net.corda.node.services.messaging.AcknowledgeHandle - public void acknowledge() - public void persistDeduplicationId() -## public static interface net.corda.testing.node.InMemoryMessagingNetwork$LatencyCalculator @org.jetbrains.annotations.NotNull public abstract java.time.Duration between(net.corda.core.messaging.SingleMessageRecipient, net.corda.core.messaging.SingleMessageRecipient) ## @@ -3979,15 +3962,15 @@ public static final class net.corda.testing.node.InMemoryMessagingNetwork$Servic ## 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.Function3) + @org.jetbrains.annotations.NotNull public net.corda.node.services.messaging.MessageHandlerRegistration addMessageHandler(String, kotlin.jvm.functions.Function2) public void cancelRedelivery(long) - @org.jetbrains.annotations.NotNull public net.corda.node.services.messaging.Message createMessage(String, byte[], net.corda.node.services.statemachine.DeduplicationId, Map) + @org.jetbrains.annotations.NotNull public net.corda.node.services.messaging.Message createMessage(String, byte[], String) @org.jetbrains.annotations.NotNull public net.corda.core.messaging.MessageRecipients getAddressOfParty(net.corda.core.node.services.PartyInfo) @org.jetbrains.annotations.NotNull public final net.corda.node.services.messaging.MessagingService getMessagingService() @org.jetbrains.annotations.NotNull public net.corda.core.messaging.SingleMessageRecipient getMyAddress() public void removeMessageHandler(net.corda.node.services.messaging.MessageHandlerRegistration) @co.paralleluniverse.fibers.Suspendable public void send(List) - @co.paralleluniverse.fibers.Suspendable public void send(net.corda.node.services.messaging.Message, net.corda.core.messaging.MessageRecipients, Long, Object) + @co.paralleluniverse.fibers.Suspendable public void send(net.corda.node.services.messaging.Message, net.corda.core.messaging.MessageRecipients, Long, Object, Map) ## public final class net.corda.testing.node.MockKeyManagementService extends net.corda.core.serialization.SingletonSerializeAsToken implements net.corda.core.node.services.KeyManagementService @org.jetbrains.annotations.NotNull public Iterable filterMyKeys(Iterable) @@ -4118,15 +4101,14 @@ public class net.corda.testing.node.MockServices extends java.lang.Object implem @org.jetbrains.annotations.NotNull public static final net.corda.node.VersionInfo getMOCK_VERSION_INFO() @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.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() @org.jetbrains.annotations.NotNull public java.sql.Connection jdbcSession() @org.jetbrains.annotations.NotNull public net.corda.core.contracts.TransactionState loadState(net.corda.core.contracts.StateRef) @org.jetbrains.annotations.NotNull public Set loadStates(Set) - @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final Properties makeTestDataSourceProperties(String, String, kotlin.jvm.functions.Function2) - @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final net.corda.nodeapi.internal.persistence.DatabaseConfig makeTestDatabaseProperties(String) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public static final Properties makeTestDataSourceProperties(String) public void recordTransactions(Iterable) public void recordTransactions(net.corda.core.node.StatesToRecord, Iterable) public void recordTransactions(boolean, Iterable) @@ -4139,8 +4121,7 @@ public class net.corda.testing.node.MockServices extends java.lang.Object implem ## public static final class net.corda.testing.node.MockServices$Companion extends java.lang.Object @org.jetbrains.annotations.NotNull public final net.corda.node.VersionInfo getMOCK_VERSION_INFO() - @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final Properties makeTestDataSourceProperties(String, String, kotlin.jvm.functions.Function2) - @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final net.corda.nodeapi.internal.persistence.DatabaseConfig makeTestDatabaseProperties(String) + @kotlin.jvm.JvmStatic @org.jetbrains.annotations.NotNull public final Properties makeTestDataSourceProperties(String) ## public static final class net.corda.testing.node.MockServices$Companion$makeTestDatabaseAndMockServices$mockService$1$1 extends net.corda.testing.node.MockServices @org.jetbrains.annotations.NotNull public net.corda.node.services.api.VaultServiceInternal getVaultService() @@ -4149,8 +4130,6 @@ public static final class net.corda.testing.node.MockServices$Companion$makeTest ## public final class net.corda.testing.node.MockServicesKt extends java.lang.Object @org.jetbrains.annotations.NotNull public static final net.corda.core.serialization.SerializeAsToken createMockCordaService(net.corda.testing.node.MockServices, kotlin.jvm.functions.Function1) - @org.jetbrains.annotations.NotNull public static final com.typesafe.config.Config databaseProviderDataSourceConfig(String, String) - @org.jetbrains.annotations.NotNull public static final com.typesafe.config.Config inMemoryH2DataSourceConfig(String, String) ## public static final class net.corda.testing.node.MockServicesKt$createMockCordaService$MockAppServiceHubImpl extends java.lang.Object implements net.corda.core.node.AppServiceHub, net.corda.core.node.ServiceHub public (net.corda.testing.node.MockServices, net.corda.testing.node.MockServices, kotlin.jvm.functions.Function1) @@ -4198,7 +4177,6 @@ public class net.corda.testing.node.MockTransactionStorage extends net.corda.cor @org.jetbrains.annotations.NotNull public net.corda.core.contracts.TransactionState loadState(net.corda.core.contracts.StateRef) @org.jetbrains.annotations.NotNull public Set loadStates(Set) @org.jetbrains.annotations.NotNull public net.corda.core.messaging.DataFeed track() - @org.jetbrains.annotations.NotNull public net.corda.core.concurrent.CordaFuture trackTransaction(net.corda.core.crypto.SecureHash) ## public final class net.corda.testing.node.NodeTestUtils extends java.lang.Object @org.jetbrains.annotations.NotNull public static final net.corda.testing.dsl.LedgerDSL ledger(net.corda.core.node.ServiceHub, kotlin.jvm.functions.Function1) @@ -4539,25 +4517,6 @@ public final class net.corda.testing.core.TestUtils extends java.lang.Object @org.jetbrains.annotations.NotNull public static final net.corda.core.identity.Party singleIdentity(net.corda.core.node.NodeInfo) @org.jetbrains.annotations.NotNull public static final net.corda.core.identity.PartyAndCertificate singleIdentityAndCert(net.corda.core.node.NodeInfo) ## -public final class net.corda.testing.database.DatabaseConstants extends java.lang.Object - @org.jetbrains.annotations.NotNull public static final String DATA_SOURCE_CLASSNAME = "dataSourceProperties.dataSourceClassName" - @org.jetbrains.annotations.NotNull public static final String DATA_SOURCE_PASSWORD = "dataSourceProperties.dataSource.password" - @org.jetbrains.annotations.NotNull public static final String DATA_SOURCE_URL = "dataSourceProperties.dataSource.url" - @org.jetbrains.annotations.NotNull public static final String DATA_SOURCE_USER = "dataSourceProperties.dataSource.user" - public static final net.corda.testing.database.DatabaseConstants INSTANCE - @org.jetbrains.annotations.NotNull public static final String SCHEMA = "database.schema" - @org.jetbrains.annotations.NotNull public static final String TRANSACTION_ISOLATION_LEVEL = "database.transactionIsolationLevel" -## -public final class net.corda.testing.database.DbScriptRunner extends java.lang.Object - @org.jetbrains.annotations.NotNull public final List merge(List, String) - @org.jetbrains.annotations.NotNull public final List merge(List, List) - public final void runDbScript(String, String, List) - public static final net.corda.testing.database.DbScriptRunner INSTANCE -## -public final class net.corda.testing.database.ListPopulator extends java.lang.Object implements org.springframework.jdbc.datasource.init.DatabasePopulator - public (boolean, boolean, List) - public void populate(java.sql.Connection) -## public final class net.corda.testing.dsl.AttachmentResolutionException extends net.corda.core.flows.FlowException public (net.corda.core.crypto.SecureHash) ## @@ -4692,7 +4651,6 @@ public final class net.corda.testing.dsl.TransactionDSL extends java.lang.Object @org.jetbrains.annotations.NotNull public net.corda.testing.dsl.EnforceVerifyOrFail failsWith(String) @org.jetbrains.annotations.NotNull public net.corda.testing.dsl.LedgerDSLInterpreter getLedgerInterpreter() public final void input(String) - public final void input(String, String) public final void input(String, net.corda.core.contracts.ContractState) public void input(net.corda.core.contracts.StateRef) public final void output(String, int, net.corda.core.contracts.ContractState) @@ -4764,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) diff --git a/build.gradle b/build.gradle index 57211a69d0..0531166061 100644 --- a/build.gradle +++ b/build.gradle @@ -123,7 +123,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' @@ -306,31 +305,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 = [] - } -} - -// User and key are commented out to prevent accidental pushes of R3 Corda to public repos. DO NOT UNCOMMENT. 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/constants.properties b/constants.properties index 49ea962adb..c134a442b1 100644 --- a/constants.properties +++ b/constants.properties @@ -1,4 +1,4 @@ -gradlePluginsVersion=4.0.0 +gradlePluginsVersion=4.0.2 kotlinVersion=1.2.20 platformVersion=2 guavaVersion=21.0 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/flows/FlowLogic.kt b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt index f02117bb7c..24951edec0 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt @@ -189,7 +189,7 @@ abstract class FlowLogic { open fun receiveAll(receiveType: Class, sessions: List, maySkipCheckpoint: Boolean = false): List> { enforceNoPrimitiveInReceive(listOf(receiveType)) enforceNoDuplicates(sessions) - return castMapValuesToKnownType(receiveAllMap(associateSessionsToReceiveType(receiveType, sessions), maySkipCheckpoint)) + return castMapValuesToKnownType(receiveAllMap(associateSessionsToReceiveType(receiveType, sessions))) } /** 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 1db993649a..294451cbd7 100644 --- a/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt @@ -148,7 +148,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/InternalUtils.kt b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt index 8a17ec5966..ac4dabae8d 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -147,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) + "…" @@ -304,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 */ @@ -377,15 +379,14 @@ 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(remaining()).also { get(it) } +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) } + /** Verifies that the correct notarisation request was signed by the counterparty. */ fun NotaryFlow.Service.validateRequest(request: NotarisationRequest, signature: NotarisationRequestSignature) { val requestingParty = otherSideSession.counterparty @@ -401,4 +402,4 @@ fun NotarisationRequest.generateSignature(serviceHub: ServiceHub): NotarisationR keyManagementService.sign(serializedRequest, myLegalIdentity) } return NotarisationRequestSignature(signature, serviceHub.myInfo.platformVersion) -} \ No newline at end of file +} 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/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/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/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/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 dbf27539df..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,6 +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.node.NetworkParameters import net.corda.core.node.ServicesForResolution import net.corda.core.node.services.AttachmentId import net.corda.core.serialization.CordaSerializable @@ -88,8 +89,7 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr resolveIdentity = { services.identityService.partyFromKey(it) }, resolveAttachment = { services.attachments.openAttachment(it) }, resolveStateRef = { services.loadState(it) }, - resolveContractAttachment = { services.cordappProvider.getContractAttachmentID(it.contract) }, - maxTransactionSize = services.networkParameters.maxTransactionSize + networkParameters = services.networkParameters ) } @@ -108,17 +108,16 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr resolveStateRef: (StateRef) -> TransactionState<*>?, resolveContractAttachment: (TransactionState) -> AttachmentId? ): LedgerTransaction { - return toLedgerTransactionInternal(resolveIdentity, resolveAttachment, resolveStateRef, resolveContractAttachment, 10485760) + return toLedgerTransactionInternal(resolveIdentity, resolveAttachment, resolveStateRef, null) } private fun toLedgerTransactionInternal( resolveIdentity: (PublicKey) -> Party?, resolveAttachment: (SecureHash) -> Attachment?, resolveStateRef: (StateRef) -> TransactionState<*>?, - resolveContractAttachment: (TransactionState) -> AttachmentId?, - maxTransactionSize: Int + networkParameters: NetworkParameters? ): LedgerTransaction { - // Look up public keys to authenticated identities. This is just a stub placeholder and will all change in future. + // Look up public keys to authenticated identities. val authenticatedArgs = commands.map { val parties = it.signers.mapNotNull { pk -> resolveIdentity(pk) } CommandWithParties(it.signers, parties, it.value) @@ -126,12 +125,9 @@ 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, maxTransactionSize) + 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 } @@ -143,8 +139,9 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr remainingTransactionSize -= size } - // Check attachment size first as they are most likely to go over the limit. - ltx.attachments.associateBy(Attachment::id).values.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) @@ -265,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/FlowTestsUtils.kt b/core/src/test/kotlin/net/corda/core/flows/FlowTestsUtils.kt index 99322028af..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 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 9482764d99..f91eb9c344 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt @@ -10,7 +10,6 @@ import net.corda.core.identity.Party import net.corda.node.services.api.IdentityServiceInternal import net.corda.testing.contracts.DummyContract import net.corda.testing.core.* -import net.corda.testing.internal.MockCordappProvider import net.corda.testing.internal.rigorousMock import net.corda.testing.node.MockServices import org.junit.Before @@ -30,7 +29,7 @@ class LedgerTransactionQueryTests { @JvmField val testSerialization = SerializationEnvironmentRule() private val keyPair = generateKeyPair() - private val services = MockServices(emptyList(), CordaX500Name("MegaCorp", "London", "GB"), + private val services = MockServices(listOf("net.corda.testing.contracts"), CordaX500Name("MegaCorp", "London", "GB"), rigorousMock().also { doReturn(null).whenever(it).partyFromKey(keyPair.public) }, keyPair) 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-flows.rst b/docs/source/api-flows.rst index fc5fef703d..6d77139704 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/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/deploying-a-node.rst b/docs/source/deploying-a-node.rst index 6918cc743d..0643ff1332 100644 --- a/docs/source/deploying-a-node.rst +++ b/docs/source/deploying-a-node.rst @@ -199,7 +199,7 @@ 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 ``cordapps`` in ``/opt/corda`` and save your CorDapp jar file to it. Alternatively, +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 diff --git a/docs/source/example-code/build.gradle b/docs/source/example-code/build.gradle index 50464033a5..d8ca430972 100644 --- a/docs/source/example-code/build.gradle +++ b/docs/source/example-code/build.gradle @@ -80,14 +80,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/kotlin/net/corda/docs/FlowCookbook.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt index 08827f1d30..880570e2df 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 * -----------------------------------**/ 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/FxTransactionBuildTutorialTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt index c7dc1ccbf5..67a4c919f7 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,7 +13,6 @@ 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 @@ -37,7 +36,6 @@ class FxTransactionBuildTutorialTest { mockNet.stopNodes() } - @Ignore("Pending fix from corda") @Test fun `Run ForeignExchangeFlow to completion`() { // Use NodeA as issuer and create some dollars 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/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/node-api/build.gradle b/node-api/build.gradle index f59ddc8cf2..c2a1231eb6 100644 --- a/node-api/build.gradle +++ b/node-api/build.gradle @@ -45,6 +45,12 @@ dependencies { compile "org.apache.curator:curator-recipes:${curator_version}" testCompile "org.apache.curator:curator-test:${curator_version}" + // 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/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/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/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/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/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/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt index c264264bc6..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,6 +13,7 @@ import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl +import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.TestIdentity @@ -59,7 +60,8 @@ class AttachmentsClassLoaderStaticContractTests { } private val serviceHub = rigorousMock().also { - doReturn(CordappProviderImpl(CordappLoader.createWithTestPackages(listOf("net.corda.nodeapi.internal")), MockCordappConfigProvider(), 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 5ee6355980..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,6 +18,7 @@ import net.corda.nodeapi.DummyContractBackdoor import net.corda.nodeapi.internal.serialization.SerializeAsTokenContextImpl import net.corda.nodeapi.internal.serialization.attachmentsClassLoaderEnabledPropertyName import net.corda.nodeapi.internal.serialization.withTokenContext +import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.TestIdentity @@ -58,12 +59,15 @@ class AttachmentsClassLoaderTests { @JvmField val testSerialization = SerializationEnvironmentRule() private val attachments = MockAttachmentStorage() - private val cordappProvider = CordappProviderImpl(CordappLoader.createDevMode(listOf(ISOLATED_CONTRACTS_JAR_PATH)), MockCordappConfigProvider(), 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 @@ -279,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) @@ -303,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/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 a23188bd02..6da1845180 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 @@ -524,7 +524,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 62ffb40f0a..2d4347d9bc 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -170,9 +170,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}" 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 2897d60049..48fbaa1a75 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 @@ -45,7 +45,7 @@ class AttachmentLoadingTests : IntegrationTest() { @JvmField val testSerialization = SerializationEnvironmentRule() private val attachments = MockAttachmentStorage() - private val provider = CordappProviderImpl(CordappLoader.createDevMode(listOf(isolatedJAR)), MockCordappConfigProvider(), 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) 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 35030d4884..4af4227678 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 @@ -57,7 +57,7 @@ class BFTNotaryServiceTests : IntegrationTest() { @Before fun before() { - mockNet = InternalMockNetwork(emptyList()) + mockNet = InternalMockNetwork(listOf("net.corda.testing.contracts")) } @After 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 4f08aae773..5a1cf8e40d 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,15 +2,17 @@ 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.node.NetworkParameters +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.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME import net.corda.testing.common.internal.testNetworkParameters +import net.corda.nodeapi.internal.network.SignedNetworkParameters import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.core.DUMMY_NOTARY_NAME @@ -70,8 +72,7 @@ class NetworkMapTest : IntegrationTest() { ) { 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/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 428b317ffa..383ef96db4 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -553,7 +553,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, checkpointStorage = DBCheckpointStorage() val metrics = MetricRegistry() attachments = NodeAttachmentService(metrics, configuration.attachmentContentCacheSizeBytes, configuration.attachmentCacheBound) - val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(), attachments) + val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(), attachments, networkParameters.whitelistedContractImplementations) val keyManagementService = makeKeyManagementService(identityService, keyPairs) _services = ServiceHubInternalImpl( identityService, 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 dd43721018..91195d9bff 100644 --- a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt @@ -14,6 +14,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 @@ -53,7 +54,7 @@ internal class CordaRPCOpsImpl( } override fun networkParametersFeed(): DataFeed { - return services.networkMapUpdater.track() + return services.networkMapUpdater.trackParametersUpdate() } override fun acceptNewNetworkParameters(parametersHash: SecureHash) { @@ -192,7 +193,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/cordapp/CordappProviderImpl.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt index a25d872124..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,10 +1,12 @@ package net.corda.node.internal.cordapp import com.google.common.collect.HashBiMap +import net.corda.core.contracts.ContractAttachment import net.corda.core.contracts.ContractClassName import net.corda.core.cordapp.Cordapp import net.corda.core.cordapp.CordappContext import net.corda.core.crypto.SecureHash +import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER import net.corda.core.internal.cordapp.CordappConfigProvider import net.corda.core.internal.createCordappContext import net.corda.core.node.services.AttachmentId @@ -17,7 +19,10 @@ 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, private val cordappConfigProvider: CordappConfigProvider, 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() @@ -25,6 +30,34 @@ open class CordappProviderImpl(private val cordappLoader: CordappLoader, private 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 @@ -42,11 +75,6 @@ open class CordappProviderImpl(private val cordappLoader: CordappLoader, private 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 * @@ -55,11 +83,16 @@ open class CordappProviderImpl(private val cordappLoader: CordappLoader, private */ 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 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 a7a4da24de..77e0618134 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 @@ -196,15 +196,15 @@ 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 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/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/resources/reference.conf b/node/src/main/resources/reference.conf index 6669453571..e7fbcfc57b 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/internal/cordapp/CordappProviderImplTests.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt index 9d12bffb05..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 @@ -2,14 +2,18 @@ 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.assertj.core.api.Assertions.assertThat -import org.junit.Assert +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull import org.junit.Before import org.junit.Test +import java.net.URL class CordappProviderImplTests { private companion object { @@ -25,6 +29,7 @@ class CordappProviderImplTests { } private lateinit var attachmentStore: AttachmentStorage + private val whitelistedContractImplementations = testNetworkParameters().whitelistedContractImplementations @Before fun setup() { @@ -33,44 +38,40 @@ class CordappProviderImplTests { @Test fun `isolated jar is loaded into the attachment store`() { - val loader = CordappLoader.createDevMode(listOf(isolatedJAR)) - val provider = CordappProviderImpl(loader, stubConfigProvider, 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, stubConfigProvider, 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, stubConfigProvider, 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 contract class`() { - val loader = CordappLoader.createDevMode(listOf(isolatedJAR)) - val provider = CordappProviderImpl(loader, stubConfigProvider, attachmentStore) + fun `test that we find an attachment for a cordapp contrat class`() { + 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 @@ -78,10 +79,15 @@ class CordappProviderImplTests { val configProvider = MockCordappConfigProvider() configProvider.cordappConfigs.put(isolatedCordappName, validConfig) val loader = CordappLoader.createDevMode(listOf(isolatedJAR)) - val provider = CordappProviderImpl(loader, configProvider, attachmentStore) + 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/services/network/NetworkMapClientTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapClientTest.kt index 6582196e27..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 @@ -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)) } 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 fd0214955d..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,17 +15,13 @@ 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 @@ -197,7 +193,7 @@ 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) @@ -229,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 86fba98581..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,15 +2,13 @@ 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.SerializationEnvironmentRule import net.corda.testing.driver.PortAllocation @@ -57,7 +55,9 @@ class NetworkParametersReaderTest { 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/transactions/MaxTransactionSizeTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/MaxTransactionSizeTests.kt index 9280781c4f..52a421a515 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 @@ -21,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 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 06fd23c188..95f552138d 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 @@ -78,7 +78,7 @@ open class MockServices private constructor( cordappLoader: CordappLoader, override val validatedTransactions: WritableTransactionStorage, override val identityService: IdentityService, - override val networkParameters: NetworkParameters, + final override val networkParameters: NetworkParameters, private val initialIdentity: TestIdentity, private val moreKeys: Array ) : ServiceHub, StateLoader by validatedTransactions { @@ -261,7 +261,7 @@ open class MockServices private constructor( return NodeInfo(listOf(NetworkHostAndPort("mock.node.services", 10000)), listOf(initialIdentity.identity), 1, serial = 1L) } override val transactionVerifierService: TransactionVerifierService get() = InMemoryTransactionVerifierService(2) - private val mockCordappProvider: MockCordappProvider = MockCordappProvider(cordappLoader, attachments) + private val mockCordappProvider: MockCordappProvider = MockCordappProvider(cordappLoader, attachments, networkParameters.whitelistedContractImplementations) override val cordappProvider: CordappProvider get() = mockCordappProvider internal fun makeVaultService(hibernateConfig: HibernateConfiguration, schemaService: SchemaService): VaultServiceInternal { 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 c191384ee8..5e13e35abe 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 @@ -18,7 +18,6 @@ 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 @@ -485,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) } } 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/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 322e9e90b7..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 @@ -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/internal/MockCordappProvider.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt index 4638d73fb3..e04b2aa580 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt @@ -2,6 +2,7 @@ package net.corda.testing.internal import net.corda.core.contracts.ContractClassName import net.corda.core.cordapp.Cordapp +import net.corda.core.internal.TEST_UPLOADER import net.corda.core.internal.cordapp.CordappImpl import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.AttachmentStorage @@ -14,9 +15,10 @@ import java.util.* class MockCordappProvider( cordappLoader: CordappLoader, attachmentStorage: AttachmentStorage, - val cordappConfigProvider: MockCordappConfigProvider = MockCordappConfigProvider() -) : CordappProviderImpl(cordappLoader, cordappConfigProvider, attachmentStorage) { - constructor(cordappLoader: CordappLoader, attachmentStorage: AttachmentStorage) : this(cordappLoader, attachmentStorage, MockCordappConfigProvider()) + 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>() @@ -33,20 +35,21 @@ class MockCordappProvider( customSchemas = emptySet(), jarPath = Paths.get("").toUri().toURL()) if (cordappRegistry.none { it.first.contractClassNames.contains(contractClassName) }) { - cordappRegistry.add(Pair(cordapp, findOrImportAttachment(contractClassName.toByteArray(), attachments))) + cordappRegistry.add(Pair(cordapp, findOrImportAttachment(listOf(contractClassName), contractClassName.toByteArray(), attachments))) } } - override fun getContractAttachmentID(contractClassName: ContractClassName): AttachmentId? = cordappRegistry.find { it.first.contractClassNames.contains(contractClassName) }?.second ?: super.getContractAttachmentID(contractClassName) + override fun getContractAttachmentID(contractClassName: ContractClassName): AttachmentId? = cordappRegistry.find { it.first.contractClassNames.contains(contractClassName) }?.second + ?: super.getContractAttachmentID(contractClassName) - private fun findOrImportAttachment(data: ByteArray, attachments: MockAttachmentStorage): AttachmentId { + private fun findOrImportAttachment(contractClassNames: List, 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/services/MockAttachmentStorage.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt index f827efb39d..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,31 +28,17 @@ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() { } } - override fun importAttachment(jar: InputStream): AttachmentId { - // JIS makes read()/readBytes() return bytes of the current file, but we want to hash the entire container here. - require(jar !is JarInputStream) + val files = HashMap>() - val bytes = getBytes(jar) + override fun importAttachment(jar: InputStream): AttachmentId = importAttachment(jar, UNKNOWN_UPLOADER, null) - val sha256 = bytes.sha256() - if (!files.containsKey(sha256)) { - files[sha256] = bytes + override fun importAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId { + return withContractsInJar(jar) { contractClassNames, inputStream -> + importAttachmentInternal(inputStream, uploader, filename, contractClassNames) } - return sha256 } - override fun importAttachment(jar: InputStream, uploader: String, filename: String): AttachmentId { - return importAttachment(jar) - } - - val files = HashMap() - - private class MockAttachment(dataLoader: () -> ByteArray, override val id: SecureHash) : AbstractAttachment(dataLoader) - - override fun openAttachment(id: SecureHash): Attachment? { - val f = files[id] ?: return null - return MockAttachment({ f }, id) - } + override fun openAttachment(id: SecureHash): Attachment? = files[id]?.first override fun queryAttachments(criteria: AttachmentQueryCriteria, sorting: AttachmentSort?): List { throw NotImplementedError("Querying for attachments not implemented") @@ -56,11 +46,6 @@ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() { 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) @@ -68,4 +53,25 @@ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() { 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/NodeController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt index 5ecb970fce..54edd309de 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 @@ -145,7 +145,8 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { modifiedTime = Instant.now(), maxMessageSize = 10485760, maxTransactionSize = Int.MAX_VALUE, - epoch = 1 + epoch = 1, + whitelistedContractImplementations = emptyMap() )) notaryIdentity = identity networkParametersCopier = parametersCopier 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 a07bc0c1ea..bd33ea9efc 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt @@ -43,7 +43,7 @@ data class GeneratedLedger( 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 }.associateBy { it.contract } + attachments.mapNotNull { it as? ContractAttachment }.flatMap { attch-> attch.allContracts.map { it to attch } }.toMap() } private val services = object : ServicesForResolution { 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 0905130562..abb0444f34 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt @@ -22,6 +22,7 @@ import net.corda.testing.internal.IntegrationTestSchemas import net.corda.testing.internal.toDatabaseSchemaName import net.corda.testing.node.NotarySpec import org.junit.ClassRule +import org.junit.Ignore import org.junit.Rule import org.junit.Test import java.util.* @@ -142,6 +143,7 @@ class VerifierTests : IntegrationTest() { } } + @Ignore("CORDA-1022") @Test fun `single verifier works with a node`() { verifierDriver(DriverParameters(